diff --git a/CHANGELOG.md b/CHANGELOG.md index 37534b83d8..ce88cd35a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,37 @@ Releases are listed newest first. - Added support for `static $x;` function-static declarations without an initializer, in both the native parser and the Magician `eval()` parser: the missing initializer desugars to `= null`, matching PHP, where `static $x;` and `static $x = null;` are identical (including `isset()` behavior). - Fixed untyped properties (instance and static) initializing to their inferred type's zero value instead of PHP's implicit `null`: `public $x;` and `public $x = null;` now read as `NULL` before the first write, `is_null()` / `=== null` observe it, and later scalar assignments keep nullable storage (the same slot layout as a typed `?T` property) instead of failing to compile (`prop_set assigning PHP type Void ...`) or crashing `var_dump()` on null array slots. Heterogeneous assignments widen the slot to `mixed`; assignments inside the class's own constructor keep the historical precise inferred type, and untyped properties with concrete defaults are unchanged. `ReflectionClass::getDefaultProperties()` and `ReflectionProperty::getDefaultValue()` now see the implicit `null` default through the same schema. - 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. +- Fixed every common symbol being declared with an alignment operand that only one object format reads correctly: `.comm`'s third operand is a power-of-two exponent on Mach-O and a byte count on ELF, and elephc emitted the Mach-O spelling everywhere. Linux builds therefore declared 3-byte alignment for 8-byte slots, which assembles cleanly and then fails to link with `relocation truncated to fit: R_AARCH64_LDST64_ABS_LO12_NC` as soon as any of them is reached by a 64-bit load — the stack-exhaustion guard's `_stack_limit` is the first symbol to do so, which took out linux-aarch64 entirely. All 172 emission sites now render the directive through one target-aware helper. +- Fixed `base64_decode()` returning wrong bytes with no diagnostic: embedded whitespace produced garbage, unpadded input was truncated, and an invalid character was folded into the output. It now follows php-src's decoder and gains the `$strict` parameter. +- Added `stripos()`, `strripos()`, `quoted_printable_encode()`, the key-preserving forms of `array_slice()` and `array_chunk()`, and `file($filename, $flags)`. +- Fixed a segfault when a shape-changing array builtin was called through a callable string (`$f = 'array_reverse'; $f([1,2,3,4]);`): the result was typed from the builtin's broad declared return instead of its real layout. +- Added PHP's scalar parameter coercion for the conversions that can be reproduced exactly (int/float/bool into a `string` parameter, int/float/string into a `bool` parameter, constant floats and numeric strings into `int`/`float`), and accepted callable strings such as `apply("strtoupper", $s)` at a `callable` parameter. The conversions PHP signals at runtime — a lossy float to int, a non-numeric string to int — are still rejected, with the PHP behavior named in the message. Corrects the documentation, which claimed "always strict typing" while `declare(strict_types=1)` was in fact parsed and discarded. +- Widened builtin signatures to PHP's real parameter lists: `implode($array)`, `array_unshift()` with several values, `array_search(..., $strict)`, `array_reverse(..., $preserve_keys)`, `range(..., $step)`, `strpos()`/`strrpos()` with `$offset`, `intval($value, $base)` and `ucwords($string, $separators)`. Also fixes `ucwords()` omitting carriage return, form feed and vertical tab from PHP's default separator set. +- Fixed array builtins that mutate by reference silently doing nothing when the receiver was an object property, a static property or an array element: `usort($obj->items, ...)` returned the array unsorted with no diagnostic, because the by-reference parameter got a value temporary instead of the caller's storage. +- Fixed reading a dynamic object property dropping a reference it never took, which freed the live value after a few reads and answered `null` from then on. +- Added `usort()` over string arrays, `array_reduce()` over string arrays with an integer or boolean accumulator, and `unset()` of dynamic object properties (`stdClass`, `#[AllowDynamicProperties]`). `unset()` of an untyped declared property, `uasort()`/`uksort()`/`array_walk()` over associative arrays, and `unset()` on a by-reference property are reported with diagnostics naming the shape instead of producing a wrong value. +- Added `func_num_args()`, `func_get_args()` and `func_get_arg()`, together with the surplus positional arguments PHP allows past a function's declared parameter list. Shapes that cannot be represented — a defaulted parameter, an existing variadic, an overridden method signature, a dynamic call, top-level use — get a targeted compile error rather than a wrong answer. +- Added `strtr()`, `count_chars()` and `str_word_count()`, completing the audit's missing-builtin list. +- Fixed `new $class(...$args)` silently dropping its spread arguments: the object was constructed with no arguments at all, or with only the named ones, and no diagnostic was reported. Dynamic `new` now shares the same call-argument planning as every other call surface. +- Added the internal array pointer family (`key()`, `current()`, `next()`, `prev()`, `reset()`, `end()`), plus `quotemeta()`, `chunk_split()` and `base_convert()`. +- Added `++`/`--` on strings with PHP's perl-style alphanumeric carry (`"az"` becomes `"ba"`, `"Zz"` becomes `"AAa"`), including the numeric-string cases where the value changes type (`"9"++` is `int(10)`). By-reference parameters and static locals are rejected with a diagnostic explaining why. +- Added `substr_count()`, `strncmp()`/`strncasecmp()`, `dechex`/`hexdec`/`decbin`/`bindec`/`decoct`/`octdec`, `array_count_values()`, `constant()`, `join()`, and the `PHP_ROUND_HALF_*`/`COUNT_*` constants. `round()` gains its `$mode` argument, implemented as php-src's algorithm — which also corrects the two-argument form, where `round(1.005, 2)` returned `1` instead of `1.01`. `count()` gains its `$mode` argument. Single-array `min()`/`max()` accept strings, associative arrays and heterogeneous elements. +- Fixed unbounded recursion crashing with a raw SIGSEGV: every function prologue now checks the stack pointer against a limit derived from `RLIMIT_STACK`, and reports PHP's "Maximum call stack size reached" instead. Fibers and generators, which run on their own stacks, carry their own floor. +- Fixed `array_slice()`, `array_splice()` and `array_pad()` trusting a negative length: the first two used `-1` as their "no length given" sentinel, which is exactly PHP's "stop one element before the end", and neither clamped other negative lengths, so results carried negative element counts and out-of-bounds contents; `array_pad()` negated its length unchecked, so `PHP_INT_MIN` walked off the allocation. All three now match PHP, and `array_pad()` raises PHP's `ValueError` beyond the maximum length. +- Fixed builtins skipping the argument validation PHP performs, where the failure mode was memory corruption or an infinite loop rather than an error: `str_pad()` with an empty pad string read uninitialized memory, `str_split()` with a chunk length of 0 exhausted the heap, `explode("")` and `array_chunk($a, 0)` looped forever, `number_format()` overran its formatting buffer for large magnitudes, `random_int()` with inverted bounds returned garbage, and `array_fill()` with a negative count produced an array whose `count()` was negative. All now raise PHP's catchable `ValueError`. `explode()` gains its `$limit` parameter and the `STR_PAD_*` constants now exist. +- Fixed `==` between two objects and between an array and a scalar or null failing to compile, and implemented `unset()` on a declared property. Cyclic graphs compared with `==` stop at a depth limit rather than overflowing the stack. +- Fixed float printing, numeric strings, `fmod()` and `**`: `echo` and `var_dump` follow PHP's two distinct precision rules (so `1.0E+300` and `float(1000000000000000)` instead of `1E+300` and `float(1E+15)`), string-to-number conversion follows PHP's grammar rather than accepting `INF`/`nan`/hex, `fmod()` keeps the sign of a negative zero, and `2 ** 3` stays an integer. +- Fixed generator keys restarting from zero after an explicit key, and made argument unpacking after a named argument the compile-time error PHP raises. +- Added PHP's alternative control-flow syntax (`if:`/`endif;`, `foreach:`, `while:`, `for:`, `switch:`), `<>`, `$this->n++`, destructuring in `foreach`, increment and decrement of floats, single-array `min()`/`max()`, `join()`, `substr_count()`, and the `PHP_ROUND_HALF_*` and `COUNT_RECURSIVE` constants. `goto` and reference elements in array literals now report clearly that they are unsupported instead of producing confusing syntax errors. +- Fixed memory corruption when a string or stream builtin produced a result larger than the shared 64 KiB concat scratch buffer: the `.` operator itself (which crashed outright past 64 KiB, and whose `.=` accumulation now allocates and frees exactly one block per append), `bin2hex()`, `base64_encode()`/`base64_decode()`, `urlencode()`/`rawurlencode()`/`urldecode()`, `hex2bin()`, `str_pad()`, `str_repeat()`, `addslashes()`/`stripslashes()`, `htmlspecialchars()`/`html_entity_decode()`, `nl2br()`, `wordwrap()`, `str_replace()`/`str_ireplace()`, `substr_replace()`, `number_format()`, `fread()`, `fgets()`, `stream_get_contents()` and `stream_get_line()` now size their result up front and fall back to owned heap storage, so large results are returned correctly instead of overwriting the stream-handle table, exception state and heap globals that follow the buffer in memory. `str_repeat()` with a `$length * $times` product that overflows a machine word now reports PHP's "Possible integer overflow in memory allocation" fatal error instead of crashing. +- Fixed `sprintf()`/`printf()` writing outside their buffers: a long conversion specifier overran a 32-byte stack buffer into the saved frame pointer and return address, and a conversion wider than 128 bytes copied live stack memory into the returned string, leaking pointers into program output. The formatter now parses each specifier into numeric state instead of copying program bytes into the C format string, which also closes a reachable `%n` (an unrecognized conversion used to be handed to libc, giving PHP source an arbitrary-write primitive). Adds the missing conversions `%b`, `%F`, `%E`, positional `%1$s` and custom padding `%'x`, and fixes `%s` truncating at 127 bytes, PHP's non-zero-padded `%e` exponent, and `%f` of `-0.0`. +- Fixed allocation sizes that overflow a machine word being handed to the allocator: `buffer_new()` stored the pre-overflow length in the header, so bounds checks passed for indexes far outside the real allocation, and `array_fill()`, `range()`, `array_pad()` and `SplFixedArray` could crash the same way. Sizes are now checked before allocation on every supported target, and `buffer_new()` rejects negative lengths. +- Fixed unrelated PHP declarations sharing one symbol: composite symbol names joined mangled fragments with an underscore, so `class a { static $u_b; }` and `class a_u { static $b; }` shared a single storage cell, and the equivalent method and enum-case collisions failed to assemble. A static local named `$x_init` also aliased the initialization flag of a static `$x`, and internal labels could collide across functions whose names differ only in non-ASCII characters. +- Fixed `--debug-info` splicing the source path into assembler directives with incomplete escaping, where a crafted path could close the directive string and have the remainder assembled as directives. Linux executables and shared libraries are now linked with `-z noexecstack`, `-z relro` and `-z now`. +- Fixed `var_dump()` printing an enum case as an ordinary object instead of `enum(Status::Active)`, `print_r()` of an object failing to compile, and `var_export()` of an object returning the empty string. All three now match PHP, including nesting, visibility annotations, `*RECURSION*`, and `\Cls::__set_state(...)`. +- Fixed `%` and `/` by zero returning `0`/`INF` instead of raising the catchable `DivisionByZeroError` PHP 8 defines, shift amounts of 64 or more wrapping to hardware behavior instead of saturating like PHP (and negative shifts not raising `ArithmeticError`), `abs(PHP_INT_MIN)` wrapping negative instead of promoting to float, and float-to-int conversion of `NAN`/`INF`/out-of-range values disagreeing both with PHP and between targets. `PHP_INT_MIN % -1` no longer raises SIGFPE on x86_64. +- Fixed four idiomatic PHP shapes being rejected at compile time: the classic singleton (assigning a nullable static property inside its own null check now narrows it), `isset()`/`empty()`/`??` on a never-declared variable, untyped closure parameters passed to `usort()`/`array_filter()`/`array_map()`/`array_walk()`/`array_reduce()` (which now inherit the array's element and key types), and namespace aliases in qualified names such as `use App\Math as M; M\double(5)`. +- Fixed constant folding computing different answers than PHP: integer and numeric-string comparisons past 2^53 went through `f64`, `switch (2) { case true: }` took the wrong branch, `PHP_INT_MIN % -1` panicked the compiler, array-literal key normalization was ignored, `0.0` and `-0.0` were merged despite the sign being observable, and `(float)"INF"`/`"nan"` folded to `INF`/`NAN`. ## [0.26.3] - 2026-08-05 - Added tagless `.lfc` source files with per-file PHP/LFC classification across entry points, includes, and autoload; LFC always enables elephc extensions, while `--strict-php` remains PHP-only and now composes with `--define`, callable dispatch, and `eval()`. diff --git a/README.md b/README.md index 48cef8631b..f3c27c42d1 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ The full list of supported constructs, operators, and control structures is in t - **Generators**: generator functions and closures, `yield`, key/value yields, `yield from`, `Generator::send()`, `throw()`, `getReturn()`, and `foreach` over `Iterator` / `IteratorAggregate` - **Fibers**: `Fiber`, `FiberError`, `Fiber::suspend()`, `Fiber::getCurrent()`, `start()`, `resume()`, `throw()`, `getReturn()`, state predicates, closure captures, guarded native stacks, and target-aware context switching on macOS ARM64, Linux ARM64, and Linux x86_64 - **Control flow**: if/elseif/else, while, do-while, for, foreach, switch, match, break/continue including multi-level depths, try/catch/finally/throw -- **Statements and literals**: `const` / `define()` constants, `global` declarations, `static` locals (with or without an initializer), `print` expressions, list unpacking, PHP numeric literal forms, heredoc / nowdoc strings, `declare(strict_types=1)` and `declare(ticks=...)` directives (validated syntactically and treated as no-ops — elephc compiles an always-strict subset) +- **Statements and literals**: `const` / `define()` constants, `global` declarations, `static` locals (with or without an initializer), `print` expressions, list unpacking, PHP numeric literal forms, heredoc / nowdoc strings, `declare(strict_types=1)` (per-file strict parameter binding, exactly as in PHP) and `declare(ticks=...)` directives - **Operators**: arithmetic, comparison, `instanceof`, logical, bitwise, ternary, null coalescing (`??`), PHP 8.5 pipe (`|>`), assignment expressions for local and stabilized non-local targets, null coalescing assignment (`??=`), error control (`@`), and compound assignments - **Types**: union types (`int|string`), nullable (`?int`), `never` return type, `iterable` pseudo-type, inferred `resource|false` values for `fopen()` and `resource` values for standard streams, type casting, typed properties, typed function, method, closure, and arrow parameters and returns - **Modules**: namespaces, use imports, include/require/include_once/require_once, compile-time Composer PSR-4/PSR-0/classmap/files autoloading, `spl_autoload_register()` rule extraction, PHP magic constants diff --git a/crates/elephc-magician/src/context/core.rs b/crates/elephc-magician/src/context/core.rs index e757874fb1..c05e8f6cc1 100644 --- a/crates/elephc-magician/src/context/core.rs +++ b/crates/elephc-magician/src/context/core.rs @@ -55,6 +55,7 @@ pub struct ElephcEvalContext { pub(super) dynamic_destructed_objects: HashSet, pub(super) dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, pub(super) array_element_aliases: HashMap<(usize, EvalArrayReferenceKey), EvalReferenceTarget>, + pub(super) array_cursors: HashMap, pub(super) dynamic_initialized_properties: HashSet<(u64, String)>, pub(super) eval_reflection_attributes: HashMap, pub(super) eval_reflection_classes: HashMap, @@ -128,6 +129,7 @@ impl ElephcEvalContext { dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), array_element_aliases: HashMap::new(), + array_cursors: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), @@ -202,6 +204,7 @@ impl ElephcEvalContext { dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), array_element_aliases: HashMap::new(), + array_cursors: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), diff --git a/crates/elephc-magician/src/context/reference_metadata.rs b/crates/elephc-magician/src/context/reference_metadata.rs index 378eb748c5..ae677c5830 100644 --- a/crates/elephc-magician/src/context/reference_metadata.rs +++ b/crates/elephc-magician/src/context/reference_metadata.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Defines callable ABI aliases, execution-scope snapshots, and reference target shapes. +//! Defines callable ABI aliases, execution-scope snapshots, reference target +//! shapes, and PHP internal array pointer state. //! //! Called from: //! - Argument binding, reference writeback, object properties, and native invokers. @@ -72,6 +73,20 @@ pub enum EvalArrayReferenceKey { String(Vec), } +/// PHP internal array pointer state tracked per runtime array cell. +/// +/// Runtime cells do not carry PHP's `zend_array` internal position, so eval +/// models it as a cursor over the array's iteration order. PHP has exactly one +/// invalid state: once the cursor runs off either end, only `reset()`/`end()` +/// bring it back. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EvalArrayCursor { + /// The pointer addresses one zero-based iteration position. + Position(usize), + /// The pointer ran off an end and no longer addresses an element. + Invalid, +} + /// Late-static dispatch metadata attached to eval-created static callable arrays. #[derive(Clone)] pub(super) struct EvalStaticCallableMetadata { diff --git a/crates/elephc-magician/src/context/runtime_state.rs b/crates/elephc-magician/src/context/runtime_state.rs index 10f4d62111..a807d3b1ec 100644 --- a/crates/elephc-magician/src/context/runtime_state.rs +++ b/crates/elephc-magician/src/context/runtime_state.rs @@ -6,6 +6,8 @@ //! //! Key details: //! - Static cells, include state, scope stacks, errors, timezone, HTTP status, and magic paths live here. +//! - Internal array pointers live here too, because runtime array cells carry no +//! PHP-visible cursor of their own. use super::*; @@ -101,6 +103,30 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Returns the PHP internal array pointer tracked for one runtime array cell. + /// + /// Cells without a stored cursor answer `Position(0)`, matching PHP, where a + /// freshly built array points at its first element. + pub fn array_cursor(&self, array: RuntimeCellHandle) -> EvalArrayCursor { + self.array_cursors + .get(&(array.as_ptr() as usize)) + .copied() + .unwrap_or(EvalArrayCursor::Position(0)) + } + + /// Stores the PHP internal array pointer for one runtime array cell. + /// + /// The default cursor is dropped instead of stored so a later array cell that + /// reuses this address starts from PHP's fresh-array state. + pub fn set_array_cursor(&mut self, array: RuntimeCellHandle, cursor: EvalArrayCursor) { + let key = array.as_ptr() as usize; + if cursor == EvalArrayCursor::Position(0) { + self.array_cursors.remove(&key); + return; + } + self.array_cursors.insert(key, cursor); + } + /// Returns true when an eval include key was already loaded by this context. pub fn has_included_file(&self, path: &str) -> bool { self.included_files.contains(path) diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs index 88c41d405c..946dd9cefa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs @@ -6,13 +6,18 @@ //! //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +//! - The parameter list mirrors PHP's +//! `array_chunk(array $array, int $length, bool $preserve_keys = false)` and must stay +//! shape-identical to the static registry declaration, which the builtin parity gate asserts. + +use super::super::spec::EvalBuiltinDefaultValue; use super::super::super::*; eval_builtin! { name: "array_chunk", area: Array, - params: [array, length], + params: [array, length, preserve_keys = EvalBuiltinDefaultValue::Bool(false)], direct: Array, values: Array, } @@ -32,29 +37,48 @@ pub(in crate::interpreter) fn eval_array_chunk_declared_values_result( _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let [array, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_chunk_result(*array, *length, values) + match evaluated_args { + [array, length] => eval_array_chunk_result(*array, *length, false, values), + [array, length, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_chunk_result(*array, *length, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } -/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +/// Evaluates PHP `array_chunk()` over array, chunk-size, and preserve-keys expressions. pub(in crate::interpreter) fn eval_builtin_array_chunk( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [array, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_chunk_result(array, length, values) + match args { + [array, length] => { + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, false, values) + } + [array, length, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_chunk_result(array, length, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } -/// Builds an `array_chunk()` result as nested reindexed arrays. +/// Builds an `array_chunk()` result as nested reindexed or key-preserving chunks. +/// +/// PHP renumbers every chunk from zero unless `$preserve_keys` is truthy, in which case each +/// chunk keeps the source keys of its own window. The outer array is always a list. pub(in crate::interpreter) fn eval_array_chunk_result( array: RuntimeCellHandle, length: RuntimeCellHandle, + preserve_keys: bool, values: &mut impl RuntimeValueOps, ) -> Result { let chunk_size = eval_int_value(length, values)?; @@ -69,14 +93,29 @@ pub(in crate::interpreter) fn eval_array_chunk_result( for chunk_index in 0..chunk_count { let start = chunk_index * chunk_size; let end = usize::min(start + chunk_size, len); - let mut chunk = values.array_new(end - start)?; + let mut keys = Vec::with_capacity(end - start); + let mut has_string_key = false; for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let value = values.array_get(array, source_key)?; - let target_index = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_index = values.int(target_index)?; - chunk = values.array_set(chunk, target_index, value)?; + let key = values.array_iter_key(array, source_position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + let mut chunk = if preserve_keys || has_string_key { + values.assoc_new(end - start)? + } else { + values.array_new(end - start)? + }; + let mut next_numeric_key = 0_i64; + for key in keys { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let target_key = values.int(next_numeric_key)?; + next_numeric_key += 1; + target_key + }; + chunk = values.array_set(chunk, target_key, value)?; } let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; let result_key = values.int(result_key)?; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs new file mode 100644 index 0000000000..64cbac17fe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs @@ -0,0 +1,84 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `array_count_values`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Only `int` and `string` elements are countable; PHP warns and skips anything +//! else, and eval skips silently because it emits no diagnostics. +//! - Counting through `array_set` means the array layer applies PHP's key +//! normalization, so the integer `1` and the string `"1"` share one bucket +//! exactly as they do in the compiled runtime. + +use super::super::super::*; + +eval_builtin! { + name: "array_count_values", + area: Array, + params: [array], + direct: Array, + values: Array, +} + +/// Dispatches direct eval calls for the `array_count_values` array builtin. +pub(in crate::interpreter) fn eval_array_count_values_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_count_values(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_count_values` array builtin. +pub(in crate::interpreter) fn eval_array_count_values_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_count_values_result(*array, values) +} + +/// Evaluates PHP `array_count_values()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_count_values( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_count_values_result(array, values) +} + +/// Builds the value-to-occurrence-count map PHP's `array_count_values()` returns. +pub(in crate::interpreter) fn eval_array_count_values_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + let one = values.int(1)?; + let present = values.array_key_exists(value, result)?; + let next = if values.truthy(present)? { + let existing = values.array_get(result, value)?; + values.add(existing, one)? + } else { + one + }; + result = values.array_set(result, value, next)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs index 0cca24d3fa..4607f30287 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs @@ -6,9 +6,21 @@ //! //! Key details: //! - Runtime behavior stays delegated to the array-pad hook. +//! - `$length` is bounded the way php-src bounds it, before any allocation: an unrepresentable +//! magnitude (`PHP_INT_MIN`) or one past the maximum allowed array size is refused instead of +//! driving the interpreter through a multi-billion-element build loop. use super::super::super::*; +/// The largest `array_pad()` `$length` magnitude reference PHP will build an array for. +/// +/// php-src rejects anything past this bound with +/// `ValueError: array_pad(): Argument #2 ($length) must not exceed the maximum allowed array size` +/// before it looks at the input array, so the bound is a plain constant. The AOT path raises that +/// catchable `ValueError`; eval refuses the call the same way it refuses a negative +/// `array_fill()` count. +const ARRAY_PAD_MAX_LENGTH: i64 = 1_073_741_824; + eval_builtin! { name: "array_pad", area: Array, @@ -53,6 +65,9 @@ pub(in crate::interpreter) fn eval_builtin_array_pad( } /// Builds an `array_pad()` result by copying values and padding left or right. +/// +/// A `$length` whose magnitude is unrepresentable or past `ARRAY_PAD_MAX_LENGTH` is refused before +/// any array is allocated, so the build loop below always runs a bounded number of times. pub(in crate::interpreter) fn eval_array_pad_result( array: RuntimeCellHandle, length: RuntimeCellHandle, @@ -63,6 +78,7 @@ pub(in crate::interpreter) fn eval_array_pad_result( let target = eval_int_value(length, values)?; let target_len = target .checked_abs() + .filter(|magnitude| *magnitude <= ARRAY_PAD_MAX_LENGTH) .ok_or(EvalStatus::RuntimeFatal) .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; let result_len = usize::max(len, target_len); diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pointer.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pointer.rs new file mode 100644 index 0000000000..28efab39a2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pointer.rs @@ -0,0 +1,104 @@ +//! Purpose: +//! Shared PHP internal array pointer state and moves behind `key`, `current`, +//! `next`, `prev`, `reset`, and `end`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array` internal-pointer builtin owners. +//! - `crate::interpreter::builtins::array::mutating_dispatch`. +//! - `crate::interpreter::builtins::registry::dynamic_mutation`. +//! +//! Key details: +//! - The pointer is a cursor over the array's iteration order, stored per runtime +//! array cell in the eval context because runtime cells carry no `zend_array` +//! internal position of their own. +//! - PHP has exactly one invalid state: once the cursor runs off either end it +//! stays invalid until `reset()` or `end()` recovers it. +//! - The by-reference movers persist the moved cursor; by-value callable dispatch +//! computes the same move over PHP's temporary copy and leaves the source alone. + +use super::super::super::*; + +/// Returns the addressable internal pointer position for one array cell, if any. +pub(in crate::interpreter) fn eval_array_pointer_position( + array: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + Ok(match context.array_cursor(array) { + EvalArrayCursor::Position(position) if position < len => Some(position), + _ => None, + }) +} + +/// Returns the moved cursor and PHP-visible result for one internal pointer mover. +pub(in crate::interpreter) fn eval_array_pointer_move( + name: &str, + array: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(EvalArrayCursor, RuntimeCellHandle), EvalStatus> { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let position = eval_array_pointer_position(array, context, values)?; + let moved = match name { + "reset" => (len > 0).then_some(0), + "end" => len.checked_sub(1), + "next" => position + .and_then(|position| position.checked_add(1)) + .filter(|moved| *moved < len), + "prev" => position.and_then(|position| position.checked_sub(1)), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let Some(moved) = moved else { + let result = values.bool_value(false)?; + return Ok((EvalArrayCursor::Invalid, result)); + }; + let value = eval_array_pointer_value(array, moved, values)?; + Ok((EvalArrayCursor::Position(moved), value)) +} + +/// Reads the array value stored at one internal pointer position. +fn eval_array_pointer_value( + array: RuntimeCellHandle, + position: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Evaluates direct by-reference internal pointer calls and stores the moved cursor. +pub(in crate::interpreter) fn eval_array_pointer_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (array, _target) = + super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values)?; + let (cursor, result) = eval_array_pointer_move(name, array, context, values)?; + context.set_array_cursor(array, cursor); + Ok(result) +} + +/// Evaluates by-value callable internal pointer calls without moving the source cursor. +pub(in crate::interpreter) fn eval_array_pointer_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + super::array_pop::eval_warn_array_by_value(name, values)?; + let (_cursor, result) = eval_array_pointer_move(name, *array, context, values)?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs index 4f87ec7ea6..cb024d0c95 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs @@ -6,6 +6,10 @@ //! //! Key details: //! - Runtime behavior stays delegated to the array-slice hook. +//! - The parameter list mirrors PHP's +//! `array_slice(array $array, int $offset, ?int $length = null, bool $preserve_keys = false)` +//! and must stay shape-identical to the static registry declaration, which the builtin parity +//! gate asserts. use super::super::spec::EvalBuiltinDefaultValue; @@ -14,7 +18,12 @@ use super::super::super::*; eval_builtin! { name: "array_slice", area: Array, - params: [array, offset, length = EvalBuiltinDefaultValue::Null], + params: [ + array, + offset, + length = EvalBuiltinDefaultValue::Null, + preserve_keys = EvalBuiltinDefaultValue::Bool(false) + ], direct: ArraySlice, values: ArraySlice, } @@ -35,13 +44,19 @@ pub(in crate::interpreter) fn eval_array_slice_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values), - [array, offset, length] => eval_array_slice_result(*array, *offset, Some(*length), values), + [array, offset] => eval_array_slice_result(*array, *offset, None, false, values), + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), false, values) + } + [array, offset, length, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_slice_result(*array, *offset, Some(*length), preserve_keys, values) + } _ => Err(EvalStatus::RuntimeFatal), } } -/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +/// Evaluates PHP `array_slice()` over array, offset, optional length, and preserve-keys expressions. pub(in crate::interpreter) fn eval_builtin_array_slice( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -52,23 +67,36 @@ pub(in crate::interpreter) fn eval_builtin_array_slice( [array, offset] => { let array = eval_expr(array, context, scope, values)?; let offset = eval_expr(offset, context, scope, values)?; - eval_array_slice_result(array, offset, None, values) + eval_array_slice_result(array, offset, None, false, values) } [array, offset, length] => { let array = eval_expr(array, context, scope, values)?; let offset = eval_expr(offset, context, scope, values)?; let length = eval_expr(length, context, scope, values)?; - eval_array_slice_result(array, offset, Some(length), values) + eval_array_slice_result(array, offset, Some(length), false, values) + } + [array, offset, length, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_slice_result(array, offset, Some(length), preserve_keys, values) } _ => Err(EvalStatus::RuntimeFatal), } } -/// Builds an `array_slice()` result with PHP offset and length bounds. +/// Builds an `array_slice()` result with PHP offset, length, and key-preservation rules. +/// +/// PHP renumbers the integer keys of the selected window from zero unless `$preserve_keys` is +/// truthy, while STRING keys are always preserved. The result therefore becomes an associative +/// container as soon as keys survive, exactly like `array_reverse()`'s key-preserving form. pub(in crate::interpreter) fn eval_array_slice_result( array: RuntimeCellHandle, offset: RuntimeCellHandle, length: Option, + preserve_keys: bool, values: &mut impl RuntimeValueOps, ) -> Result { let len = values.array_len(array)?; @@ -81,13 +109,29 @@ pub(in crate::interpreter) fn eval_array_slice_result( _ => len, }; - let mut result = values.array_new(end.saturating_sub(start))?; + let mut keys = Vec::with_capacity(end.saturating_sub(start)); + let mut has_string_key = false; for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; + let key = values.array_iter_key(array, source_position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(end.saturating_sub(start))? + } else { + values.array_new(end.saturating_sub(start))? + }; + let mut next_numeric_key = 0_i64; + for key in keys { + let source_value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let target_key = values.int(next_numeric_key)?; + next_numeric_key += 1; + target_key + }; result = values.array_set(result, target_key, source_value)?; } Ok(result) diff --git a/crates/elephc-magician/src/interpreter/builtins/array/current.rs b/crates/elephc-magician/src/interpreter/builtins/array/current.rs new file mode 100644 index 0000000000..82da443abc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/current.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `current`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - `current()` reads the internal array pointer without moving it, so it takes +//! the array by value like PHP. +//! - An invalidated pointer answers PHP false. + +use super::super::super::*; + +eval_builtin! { + name: "current", + area: Array, + params: [array], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `current` array builtin. +pub(in crate::interpreter) fn eval_current_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_current_result(array, context, values) +} + +/// Dispatches evaluated-argument eval calls for the `current` array builtin. +pub(in crate::interpreter) fn eval_current_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_current_result(*array, context, values) +} + +/// Returns the value at the array's internal pointer, or PHP false when invalidated. +pub(in crate::interpreter) fn eval_current_result( + array: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let Some(position) = super::array_pointer::eval_array_pointer_position(array, context, values)? + else { + return values.bool_value(false); + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs index 0ecfdd1af4..6b0ba9ded7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs @@ -23,6 +23,7 @@ pub(in crate::interpreter) fn eval_builtin_array_declared_call( "array_chunk" => super::array_chunk::eval_array_chunk_declared_call(args, context, scope, values), "array_column" => super::array_column::eval_array_column_declared_call(args, context, scope, values), "array_combine" => super::array_combine::eval_array_combine_declared_call(args, context, scope, values), + "array_count_values" => super::array_count_values::eval_array_count_values_declared_call(args, context, scope, values), "array_diff" => super::array_diff::eval_array_diff_declared_call(args, context, scope, values), "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_call(args, context, scope, values), "array_fill" => super::array_fill::eval_array_fill_declared_call(args, context, scope, values), @@ -48,6 +49,8 @@ pub(in crate::interpreter) fn eval_builtin_array_declared_call( "array_unique" => super::array_unique::eval_array_unique_declared_call(args, context, scope, values), "array_values" => super::array_values::eval_array_values_declared_call(args, context, scope, values), "count" => super::count::eval_count_declared_call(args, context, scope, values), + "current" => super::current::eval_current_declared_call(args, context, scope, values), + "key" => super::key::eval_key_declared_call(args, context, scope, values), "range" => super::range::eval_range_declared_call(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/end.rs b/crates/elephc-magician/src/interpreter/builtins/array/end.rs new file mode 100644 index 0000000000..34f0ced1e5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/end.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Declarative eval registry entry for `end`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path so the advanced +//! internal pointer is recorded against the caller's array cell. + +use super::super::super::*; + +eval_builtin! { + name: "end", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `end` internal pointer builtin. +pub(in crate::interpreter) fn eval_end_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_pointer::eval_array_pointer_values_result("end", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/key.rs b/crates/elephc-magician/src/interpreter/builtins/array/key.rs new file mode 100644 index 0000000000..49a1c09c0b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/key.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `key`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - `key()` reads the internal array pointer without moving it, so it takes the +//! array by value like PHP. +//! - An invalidated pointer answers PHP null. + +use super::super::super::*; + +eval_builtin! { + name: "key", + area: Array, + params: [array], + direct: Array, + values: Array, +} +/// Dispatches direct eval calls for the `key` array builtin. +pub(in crate::interpreter) fn eval_key_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_key_result(array, context, values) +} + +/// Dispatches evaluated-argument eval calls for the `key` array builtin. +pub(in crate::interpreter) fn eval_key_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_key_result(*array, context, values) +} + +/// Returns the key at the array's internal pointer, or PHP null when invalidated. +pub(in crate::interpreter) fn eval_key_result( + array: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + match super::array_pointer::eval_array_pointer_position(array, context, values)? { + Some(position) => values.array_iter_key(array, position), + None => values.null(), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index ed9b8dc282..faf6559fd4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -11,6 +11,7 @@ mod array_chunk; mod array_column; mod array_combine; +mod array_count_values; mod array_diff; mod array_diff_key; mod array_fill; @@ -24,6 +25,7 @@ mod array_keys; mod array_map; mod array_merge; mod array_pad; +mod array_pointer; mod array_pop; mod array_product; mod array_push; @@ -42,18 +44,24 @@ mod array_walk; mod arsort; mod asort; mod count; +mod current; mod direct_dispatch; +mod end; mod in_array; mod iterator_apply; mod iterator_count; mod iterator_to_array; +mod key; mod krsort; mod ksort; mod mutating_dispatch; mod mutation; mod natcasesort; mod natsort; +mod next; +mod prev; mod range; +mod reset; mod rsort; mod shuffle; mod sort; @@ -62,6 +70,7 @@ mod uksort; mod usort; mod values_dispatch; +pub(in crate::interpreter) use array_pointer::eval_array_pointer_move; pub(in crate::interpreter) use array_pop::eval_array_pop_shift_replacement; pub(in crate::interpreter) use array_push::{ eval_array_push_unshift_count_result, eval_array_push_unshift_replacement, diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs index 769c6dc513..10ea3b5088 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs @@ -28,6 +28,11 @@ pub(in crate::interpreter) fn eval_builtin_array_mutating_declared_call( } "array_splice" => super::array_splice::eval_builtin_array_splice_call(args, context, scope, values), "array_walk" => super::array_walk::eval_builtin_array_walk_call(args, context, scope, values), + "end" | "next" | "prev" | "reset" => { + super::array_pointer::eval_array_pointer_declared_call( + name, args, context, scope, values, + ) + } "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { super::sort::eval_array_sort_declared_call(name, args, context, scope, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/array/next.rs b/crates/elephc-magician/src/interpreter/builtins/array/next.rs new file mode 100644 index 0000000000..48f5992286 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/next.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Declarative eval registry entry for `next`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path so the moved +//! internal pointer is recorded against the caller's array cell. + +use super::super::super::*; + +eval_builtin! { + name: "next", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `next` internal pointer builtin. +pub(in crate::interpreter) fn eval_next_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_pointer::eval_array_pointer_values_result("next", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/prev.rs b/crates/elephc-magician/src/interpreter/builtins/array/prev.rs new file mode 100644 index 0000000000..202bb340c8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/prev.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Declarative eval registry entry for `prev`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path so the moved +//! internal pointer is recorded against the caller's array cell. + +use super::super::super::*; + +eval_builtin! { + name: "prev", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `prev` internal pointer builtin. +pub(in crate::interpreter) fn eval_prev_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_pointer::eval_array_pointer_values_result("prev", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/range.rs b/crates/elephc-magician/src/interpreter/builtins/array/range.rs index 2d9815f184..1ed8783752 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/range.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/range.rs @@ -6,13 +6,17 @@ //! //! Key details: //! - Runtime behavior stays delegated to the integer range hook. +//! - PHP's optional `$step` is accepted; its sign never chooses the traversal direction +//! (`$start` vs `$end` does), matching php-src and the AOT `__rt_range` helper. + +use super::super::spec::EvalBuiltinDefaultValue; use super::super::super::*; eval_builtin! { name: "range", area: Array, - params: [start, end], + params: [start, end, step = EvalBuiltinDefaultValue::Int(1)], direct: Range, values: Range, } @@ -32,41 +36,72 @@ pub(in crate::interpreter) fn eval_range_declared_values_result( _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let [start, end] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_range_result(*start, *end, values) + let ([start, end], step) = match evaluated_args { + [start, end] => ([*start, *end], None), + [start, end, step] => ([*start, *end], Some(*step)), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_range_result(start, end, step, values) } -/// Evaluates PHP `range()` over integer-compatible start and end expressions. +/// Evaluates PHP `range()` over integer-compatible start, end, and step expressions. pub(in crate::interpreter) fn eval_builtin_range( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [start, end] = args else { - return Err(EvalStatus::RuntimeFatal); + let (start, end, step) = match args { + [start, end] => (start, end, None), + [start, end, step] => (start, end, Some(step)), + _ => return Err(EvalStatus::RuntimeFatal), }; let start = eval_expr(start, context, scope, values)?; let end = eval_expr(end, context, scope, values)?; - eval_range_result(start, end, values) + let step = match step { + Some(step) => Some(eval_expr(step, context, scope, values)?), + None => None, + }; + eval_range_result(start, end, step, values) } /// Builds an inclusive ascending or descending integer `range()` result. +/// +/// `step` is optional and defaults to 1. Its sign is ignored: the direction comes from the +/// endpoints, exactly like php-src. PHP's three `$step` `ValueError`s (zero step, negative step +/// on an increasing range, step wider than the spanned interval) surface as `RuntimeFatal`. pub(in crate::interpreter) fn eval_range_result( start: RuntimeCellHandle, end: RuntimeCellHandle, + step: Option, values: &mut impl RuntimeValueOps, ) -> Result { let start = eval_int_value(start, values)?; let end = eval_int_value(end, values)?; + let requested_step = match step { + Some(step) => eval_int_value(step, values)?, + None => 1, + }; + if requested_step == 0 { + return Err(EvalStatus::RuntimeFatal); + } + if start < end && requested_step < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let magnitude = requested_step.checked_abs().ok_or(EvalStatus::RuntimeFatal)?; let distance = if start <= end { end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? } else { start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? }; - let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + if start != end && magnitude > distance { + return Err(EvalStatus::RuntimeFatal); + } + let count = (distance / magnitude) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let step = if start <= end { 1_i64 } else { -1_i64 }; + let step = if start <= end { magnitude } else { -magnitude }; let mut current = start; let mut result = values.array_new(count)?; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/reset.rs b/crates/elephc-magician/src/interpreter/builtins/array/reset.rs new file mode 100644 index 0000000000..199f231908 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/reset.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Declarative eval registry entry for `reset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path so the rewound +//! internal pointer is recorded against the caller's array cell. + +use super::super::super::*; + +eval_builtin! { + name: "reset", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} +/// Dispatches by-value callable eval calls for the `reset` internal pointer builtin. +pub(in crate::interpreter) fn eval_reset_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::array_pointer::eval_array_pointer_values_result("reset", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs index 3b7ab6a749..61f7519674 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs @@ -22,6 +22,7 @@ pub(in crate::interpreter) fn eval_array_declared_values_result( "array_chunk" => super::array_chunk::eval_array_chunk_declared_values_result(evaluated_args, context, values), "array_column" => super::array_column::eval_array_column_declared_values_result(evaluated_args, context, values), "array_combine" => super::array_combine::eval_array_combine_declared_values_result(evaluated_args, context, values), + "array_count_values" => super::array_count_values::eval_array_count_values_declared_values_result(evaluated_args, context, values), "array_diff" => super::array_diff::eval_array_diff_declared_values_result(evaluated_args, context, values), "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_values_result(evaluated_args, context, values), "array_fill" => super::array_fill::eval_array_fill_declared_values_result(evaluated_args, context, values), @@ -47,6 +48,8 @@ pub(in crate::interpreter) fn eval_array_declared_values_result( "array_unique" => super::array_unique::eval_array_unique_declared_values_result(evaluated_args, context, values), "array_values" => super::array_values::eval_array_values_declared_values_result(evaluated_args, context, values), "count" => super::count::eval_count_declared_values_result(evaluated_args, context, values), + "current" => super::current::eval_current_declared_values_result(evaluated_args, context, values), + "key" => super::key::eval_key_declared_values_result(evaluated_args, context, values), "range" => super::range::eval_range_declared_values_result(evaluated_args, context, values), "array_walk" => super::array_walk::eval_array_walk_declared_values_result(evaluated_args, context, values), "array_pop" => super::array_pop::eval_array_pop_declared_values_result(evaluated_args, context, values), @@ -54,6 +57,10 @@ pub(in crate::interpreter) fn eval_array_declared_values_result( "array_push" => super::array_push::eval_array_push_declared_values_result(evaluated_args, context, values), "array_unshift" => super::array_unshift::eval_array_unshift_declared_values_result(evaluated_args, context, values), "array_splice" => super::array_splice::eval_array_splice_declared_values_result(evaluated_args, context, values), + "end" => super::end::eval_end_declared_values_result(evaluated_args, context, values), + "next" => super::next::eval_next_declared_values_result(evaluated_args, context, values), + "prev" => super::prev::eval_prev_declared_values_result(evaluated_args, context, values), + "reset" => super::reset::eval_reset_declared_values_result(evaluated_args, context, values), "arsort" => super::arsort::eval_arsort_declared_values_result(evaluated_args, context, values), "asort" => super::asort::eval_asort_declared_values_result(evaluated_args, context, values), "krsort" => super::krsort::eval_krsort_declared_values_result(evaluated_args, context, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/core/constant.rs b/crates/elephc-magician/src/interpreter/builtins/core/constant.rs new file mode 100644 index 0000000000..7cd6d40d5f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/constant.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `constant`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Reuses `define`'s constant-name normalizer and the shared dynamic-constant +//! fetch, so `constant()`, `defined()` and a bare constant reference all resolve +//! the same name to the same value. +//! - An undefined name is a PHP `Error`; eval reports it as a runtime fatal because +//! the interpreter has no catchable-throw channel for builtin failures. + +use super::define::eval_constant_name; +use super::super::super::*; + +eval_builtin! { + name: "constant", + area: Core, + params: [name], + direct: Core, + values: Core, +} + +/// Evaluates `constant(name)` against eval dynamic and predefined constant names. +pub(in crate::interpreter) fn eval_builtin_constant( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_constant_lookup(name, context, values) +} + +/// Evaluates `constant(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_constant_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_constant_lookup(*name, context, values) +} + +/// Normalizes one dynamic constant name and returns its retained value. +fn eval_constant_lookup( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + eval_const_fetch(&name, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs index 3d4b718e60..3705476635 100644 --- a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs @@ -14,6 +14,7 @@ use super::super::*; mod call_user_func; mod call_user_func_array; +mod constant; mod define; mod defined; mod die; @@ -36,6 +37,7 @@ mod var_dump; pub(in crate::interpreter) use call_user_func::*; pub(in crate::interpreter) use call_user_func_array::*; +pub(in crate::interpreter) use constant::*; pub(in crate::interpreter) use define::*; pub(in crate::interpreter) use defined::*; pub(in crate::interpreter) use die::*; @@ -67,6 +69,7 @@ pub(in crate::interpreter) fn eval_builtin_core_call( match name { "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "constant" => eval_builtin_constant(args, context, scope, values), "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "die" => eval_builtin_die(args, context, scope, values), @@ -107,6 +110,7 @@ pub(in crate::interpreter) fn eval_core_values_result( }; eval_call_user_func_array_with_values(*callback, *arg_array, context, values) } + "constant" => eval_constant_result(evaluated_args, context, values), "define" => eval_define_result(evaluated_args, context, values), "defined" => eval_defined_result(evaluated_args, context, values), "die" => eval_die_values_result(evaluated_args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs index c9343252a6..2cb18e9e5d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs @@ -6,11 +6,13 @@ //! //! Key details: //! - Runtime dispatch is declared here and delegated through the file-lines helper. +//! - The parameter list mirrors PHP's `file(string $filename, int $flags = 0)` and must stay +//! shape-identical to the static registry declaration, which the builtin parity gate asserts. eval_builtin! { name: "file", area: Filesystem, - params: [filename], + params: [filename, flags = EvalBuiltinDefaultValue::Int(0)], direct: Filesystem, values: Filesystem, } @@ -35,28 +37,47 @@ pub(in crate::interpreter) fn eval_file_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { match evaluated_args { - [filename] => eval_file_result(*filename, context, values), + [filename] => eval_file_result(*filename, 0, context, values), + [filename, flags] => { + let flags = eval_int_value(*flags, values)?; + eval_file_result(*filename, flags, context, values) + } _ => Err(EvalStatus::RuntimeFatal), } } -/// Evaluates PHP `file($filename)` over one eval expression. +/// Evaluates PHP `file($filename, $flags)` over its eval expressions. pub(in crate::interpreter) fn eval_builtin_file( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_result(filename, context, values) + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, 0, context, values) + } + [filename, flags] => { + let filename = eval_expr(filename, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let flags = eval_int_value(flags, values)?; + eval_file_result(filename, flags, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } +/// PHP's `FILE_IGNORE_NEW_LINES`: drop each line's trailing `\n`, and a `\r` before it. +const EVAL_FILE_IGNORE_NEW_LINES: i64 = 2; + +/// PHP's `FILE_SKIP_EMPTY_LINES`: drop lines that are empty after the newline handling above. +const EVAL_FILE_SKIP_EMPTY_LINES: i64 = 4; + /// Reads one local file or supported wrapper and returns indexed line byte strings. pub(in crate::interpreter) fn eval_file_result( filename: RuntimeCellHandle, + flags: i64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -64,7 +85,7 @@ pub(in crate::interpreter) fn eval_file_result( if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { if values.type_tag(result)? == EVAL_TAG_STRING { let bytes = values.string_bytes(result)?; - return eval_file_lines_array(&bytes, values); + return eval_file_lines_array(&bytes, flags, values); } values.warning("Warning: file_get_contents(): Failed to open stream\n")?; return values.array_new(0); @@ -76,28 +97,51 @@ pub(in crate::interpreter) fn eval_file_result( return values.array_new(0); } }; - eval_file_lines_array(&bytes, values) + eval_file_lines_array(&bytes, flags, values) } -/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +/// Splits file payload bytes into runtime array entries, honoring PHP's `file()` flags. +/// +/// Trailing newlines are preserved unless `FILE_IGNORE_NEW_LINES` is set, and `FILE_SKIP_EMPTY_LINES` +/// is applied AFTER that trimming — which is why it alone keeps a bare `"\n"` line, exactly like +/// php-src. Result keys are always renumbered from zero over the lines that survive. fn eval_file_lines_array( bytes: &[u8], + flags: i64, values: &mut impl RuntimeValueOps, ) -> Result { + let ignore_new_lines = flags & EVAL_FILE_IGNORE_NEW_LINES != 0; + let skip_empty_lines = flags & EVAL_FILE_SKIP_EMPTY_LINES != 0; let mut result = values.array_new(0)?; let mut line_start = 0; let mut line_index = 0; + let push = |line: &[u8], + result: RuntimeCellHandle, + line_index: &mut usize, + values: &mut _| + -> Result { + let mut line = line; + if ignore_new_lines { + if let Some(trimmed) = line.strip_suffix(b"\n") { + line = trimmed.strip_suffix(b"\r").unwrap_or(trimmed); + } + } + if skip_empty_lines && line.is_empty() { + return Ok(result); + } + let result = super::scandir::eval_array_set_indexed_bytes(result, *line_index, line, values)?; + *line_index += 1; + Ok(result) + }; for (index, byte) in bytes.iter().enumerate() { if *byte != b'\n' { continue; } - result = - super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + result = push(&bytes[line_start..=index], result, &mut line_index, values)?; line_start = index + 1; - line_index += 1; } if line_start < bytes.len() { - result = super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + result = push(&bytes[line_start..], result, &mut line_index, values)?; } Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs index 1ffa82f2e3..72e590f659 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs @@ -6,11 +6,31 @@ //! //! Key details: //! - Runtime dispatch is declared here and delegated through the one-shot file read helper. +//! - The parameter list mirrors PHP's +//! `file_get_contents(string $filename, bool $use_include_path = false, +//! ?resource $context = null, int $offset = 0, ?int $length = null)` and must stay +//! shape-identical to the static registry declaration, which the builtin parity gate asserts. +//! - `$offset`/`$length` are applied to the bytes the read produced. That is what PHP's +//! seek-then-read produces for a seekable stream, and it keeps the kept byte count bounded by +//! the bytes actually available. +//! - A negative `$length` is php-src's catchable `ValueError`, raised BEFORE the file is opened +//! so a missing file plus a negative length throws instead of warning. +//! - `$use_include_path` is accepted and behaves as `false`: eval resolves paths against the +//! current directory only, which is what an include path of `"."` would do anyway. +//! - A non-null `$context` is refused rather than ignored, matching the compiler backend. + +use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { name: "file_get_contents", area: Filesystem, - params: [filename], + params: [ + filename, + use_include_path = EvalBuiltinDefaultValue::Bool(false), + context = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(0), + length = EvalBuiltinDefaultValue::Null, + ], direct: Filesystem, values: Filesystem, } @@ -19,6 +39,19 @@ use super::super::super::*; use super::*; use crate::stream_wrappers; +/// php-src's `ValueError` for a negative `file_get_contents()` `$length`. +const FILE_GET_CONTENTS_NEGATIVE_LENGTH_MESSAGE: &str = + "file_get_contents(): Argument #5 ($length) must be greater than or equal to 0"; + +/// The `$offset`/`$length` window a `file_get_contents()` call applies to the bytes it read. +#[derive(Clone, Copy)] +struct EvalFileReadRange { + /// PHP's `$offset`: non-negative counts from the start, negative counts from the end. + offset: i64, + /// PHP's `$length`, or `None` when the caller passed `null` / omitted it. + length: Option, +} + /// Dispatches direct eval calls for the `file_get_contents` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_file_get_contents_declared_call( args: &[EvalExpr], @@ -35,38 +68,61 @@ pub(in crate::interpreter) fn eval_file_get_contents_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { - [filename] => eval_file_get_contents_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), + let Some(filename) = evaluated_args.first().copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + if evaluated_args.len() > 5 { + return Err(EvalStatus::RuntimeFatal); } + eval_file_get_contents_reject_context(evaluated_args.get(2).copied(), values)?; + let range = eval_file_get_contents_range( + evaluated_args.get(3).copied(), + evaluated_args.get(4).copied(), + context, + values, + )?; + eval_file_get_contents_windowed_result(filename, range, context, values) } -/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +/// Evaluates PHP `file_get_contents($filename, …)` over its eval expressions. pub(in crate::interpreter) fn eval_builtin_file_get_contents( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [filename] = args else { + if args.is_empty() || args.len() > 5 { return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_get_contents_result(filename, context, values) + } + let mut evaluated = Vec::with_capacity(args.len()); + for arg in args { + evaluated.push(eval_expr(arg, context, scope, values)?); + } + eval_file_get_contents_declared_values_result(&evaluated, context, values) } -/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. -pub(in crate::interpreter) fn eval_file_get_contents_result( +/// Reads one path and applies PHP's `$offset`/`$length` window to the bytes it produced. +fn eval_file_get_contents_windowed_result( filename: RuntimeCellHandle, + range: EvalFileReadRange, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { - return Ok(result); + return eval_file_get_contents_window_cell(result, range, values); } match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => values.string_bytes_value(&bytes), + Ok(bytes) => match eval_file_get_contents_window_bytes(&bytes, range) { + Some(window) => values.string_bytes_value(window), + None => { + values.warning(&format!( + "Warning: file_get_contents(): Failed to seek to position {} in the stream\n", + range.offset + ))?; + values.bool_value(false) + } + }, Err(_) => { values.warning("Warning: file_get_contents(): Failed to open stream\n")?; values.bool_value(false) @@ -74,6 +130,109 @@ pub(in crate::interpreter) fn eval_file_get_contents_result( } } +/// Applies the window to a user stream wrapper's already-built result cell. +/// +/// A wrapper that answered `false` stays `false`; a string result is windowed like a file read. +fn eval_file_get_contents_window_cell( + result: RuntimeCellHandle, + range: EvalFileReadRange, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(range.offset, 0) && range.length.is_none() { + return Ok(result); + } + if values.type_tag(result)? != EVAL_TAG_STRING { + return Ok(result); + } + let bytes = values.string_bytes(result)?; + match eval_file_get_contents_window_bytes(&bytes, range) { + Some(window) => values.string_bytes_value(window), + None => { + values.warning(&format!( + "Warning: file_get_contents(): Failed to seek to position {} in the stream\n", + range.offset + ))?; + values.bool_value(false) + } + } +} + +/// Returns the requested byte window, or `None` when the seek lands before the first byte. +/// +/// A non-negative `$offset` past the end is not an error in PHP: the stream seeks there and the +/// read answers with an empty string. Only a negative `$offset` whose magnitude exceeds the byte +/// count fails the seek. The kept byte count is bounded by the bytes that remain after the start +/// position, so a huge `$length` can never index past the buffer. +fn eval_file_get_contents_window_bytes(bytes: &[u8], range: EvalFileReadRange) -> Option<&[u8]> { + let len = i64::try_from(bytes.len()).ok()?; + let start = if range.offset < 0 { + len.checked_add(range.offset)? + } else { + range.offset + }; + if start < 0 { + return None; + } + let start = start.min(len) as usize; + let available = bytes.len() - start; + let take = match range.length { + Some(length) if length < available as i64 => length.max(0) as usize, + _ => available, + }; + Some(&bytes[start..start + take]) +} + +/// Builds the `$offset`/`$length` window, raising php-src's negative-`$length` `ValueError`. +fn eval_file_get_contents_range( + offset: Option, + length: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset = match offset { + Some(offset) => eval_int_value(offset, values)?, + None => 0, + }; + let length = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + let length = eval_int_value(length, values)?; + if length < 0 { + return eval_file_get_contents_negative_length_error(context, values); + } + Some(length) + } + _ => None, + }; + Ok(EvalFileReadRange { offset, length }) +} + +/// Refuses a non-null `$context` instead of silently dropping the caller's stream options. +fn eval_file_get_contents_reject_context( + context: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(context) = context else { + return Ok(()); + }; + if values.type_tag(context)? == EVAL_TAG_NULL { + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Raises PHP's catchable `ValueError` for a negative `file_get_contents()` `$length`. +fn eval_file_get_contents_negative_length_error( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(FILE_GET_CONTENTS_NEGATIVE_LENGTH_MESSAGE)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Reads bytes from supported direct path or stream-wrapper URLs. pub(in crate::interpreter) fn eval_read_path_or_wrapper_bytes( path: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 5cdb58894a..14057f81b6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -44,6 +44,8 @@ pub(in crate::interpreter) enum EvalDirectHook { ArrayUnique, /// Dispatches `array_values(...)`. ArrayValues, + /// Dispatches `base_convert(...)`. + BaseConvert, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -56,12 +58,16 @@ pub(in crate::interpreter) enum EvalDirectHook { Ceil, /// Dispatches `chr(...)`. Chr, + /// Dispatches `chunk_split(...)`. + ChunkSplit, /// Dispatches `clamp(...)`. Clamp, /// Dispatches `count(...)`. Count, /// Dispatches core callable, constant, process-control, and debug-output builtins. Core, + /// Dispatches `count_chars(...)`. + CountChars, /// Dispatches `crc32(...)`. Crc32, /// Dispatches `ctype_*` predicates. @@ -196,6 +202,10 @@ pub(in crate::interpreter) enum EvalDirectHook { Pow, /// Dispatches `mt_rand(...)`. MtRand, + /// Dispatches `quotemeta(...)`. + QuoteMeta, + /// Dispatches `quoted_printable_encode(...)`. + QuotedPrintableEncode, /// Dispatches `rad2deg(...)`. Rad2deg, /// Dispatches `rand(...)`. @@ -282,6 +292,8 @@ pub(in crate::interpreter) enum EvalDirectHook { StrReplace, /// Dispatches `str_split(...)`. StrSplit, + /// Dispatches `str_word_count(...)`. + StrWordCount, /// Dispatches `strlen(...)` and `mb_strlen(...)`. Strlen, /// Dispatches `str_repeat(...)`. @@ -290,6 +302,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Strval, /// Dispatches `strrev(...)`. Strrev, + /// Dispatches `strtr(...)`. + Strtr, /// Dispatches `strstr(...)`. Strstr, /// Dispatches `substr(...)`. @@ -348,16 +362,19 @@ impl EvalDirectHook { Self::Asin => eval_builtin_asin(args, context, scope, values), Self::Atan => eval_builtin_atan(args, context, scope, values), Self::Atan2 => eval_builtin_atan2(args, context, scope, values), + Self::BaseConvert => eval_builtin_base_convert(args, context, scope, values), Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), Self::Boolval => eval_builtin_boolval(args, context, scope, values), Self::Ceil => eval_builtin_ceil(args, context, scope, values), Self::Chr => eval_builtin_chr(args, context, scope, values), + Self::ChunkSplit => eval_builtin_chunk_split(args, context, scope, values), Self::Clamp => eval_builtin_clamp(args, context, scope, values), Self::Core => eval_builtin_core_call(name, args, context, scope, values), Self::Cos => eval_builtin_cos(args, context, scope, values), Self::Cosh => eval_builtin_cosh(args, context, scope, values), + Self::CountChars => eval_builtin_count_chars(args, context, scope, values), Self::Crc32 => eval_builtin_crc32(args, context, scope, values), Self::Ctype => match name { "ctype_alnum" => eval_builtin_ctype_alnum(args, context, scope, values), @@ -447,6 +464,10 @@ impl EvalDirectHook { Self::Pi => eval_builtin_pi(args, values), Self::Printf => eval_builtin_printf(args, context, scope, values), Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::QuoteMeta => eval_builtin_quotemeta(args, context, scope, values), + Self::QuotedPrintableEncode => { + eval_builtin_quoted_printable_encode(args, context, scope, values) + } Self::Rad2deg => eval_builtin_rad2deg(args, context, scope, values), Self::Rand => eval_builtin_rand(args, context, scope, values), Self::RandomInt => eval_builtin_random_int(args, context, scope, values), @@ -500,7 +521,9 @@ impl EvalDirectHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::StringPosition => match name { + "stripos" => eval_builtin_stripos(args, context, scope, values), "strpos" => eval_builtin_strpos(args, context, scope, values), + "strripos" => eval_builtin_strripos(args, context, scope, values), "strrpos" => eval_builtin_strrpos(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), }, @@ -537,6 +560,7 @@ impl EvalDirectHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::StrSplit => eval_builtin_str_split(args, context, scope, values), + Self::StrWordCount => eval_builtin_str_word_count(args, context, scope, values), Self::Strlen => match name { "mb_strlen" => eval_builtin_mb_strlen(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), @@ -545,6 +569,7 @@ impl EvalDirectHook { Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), Self::Strval => eval_builtin_strval(args, context, scope, values), Self::Strrev => eval_builtin_strrev(args, context, scope, values), + Self::Strtr => eval_builtin_strtr(args, context, scope, values), Self::Strstr => eval_builtin_strstr(args, context, scope, values), Self::Substr => eval_builtin_substr(args, context, scope, values), Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 0c78325815..8efee5c2c6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -47,6 +47,8 @@ pub(in crate::interpreter) enum EvalValuesHook { ArrayUnique, /// Dispatches `array_values(...)`. ArrayValues, + /// Dispatches `base_convert(...)`. + BaseConvert, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -59,12 +61,16 @@ pub(in crate::interpreter) enum EvalValuesHook { Ceil, /// Dispatches `chr(...)`. Chr, + /// Dispatches `chunk_split(...)`. + ChunkSplit, /// Dispatches `clamp(...)`. Clamp, /// Dispatches `count(...)`. Count, /// Dispatches core callable, constant, process-control, and debug-output builtins. Core, + /// Dispatches `count_chars(...)`. + CountChars, /// Dispatches `crc32(...)`. Crc32, /// Dispatches `ctype_*` predicates. @@ -199,6 +205,10 @@ pub(in crate::interpreter) enum EvalValuesHook { Pow, /// Dispatches `mt_rand(...)`. MtRand, + /// Dispatches `quotemeta(...)`. + QuoteMeta, + /// Dispatches `quoted_printable_encode(...)`. + QuotedPrintableEncode, /// Dispatches `rad2deg(...)`. Rad2deg, /// Dispatches `rand(...)`. @@ -287,6 +297,8 @@ pub(in crate::interpreter) enum EvalValuesHook { StrReplace, /// Dispatches `str_split(...)`. StrSplit, + /// Dispatches `str_word_count(...)`. + StrWordCount, /// Dispatches `strlen(...)` and `mb_strlen(...)`. Strlen, /// Dispatches `str_repeat(...)`. @@ -295,6 +307,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Strval, /// Dispatches `strrev(...)`. Strrev, + /// Dispatches `strtr(...)`. + Strtr, /// Dispatches `strstr(...)`. Strstr, /// Dispatches `substr(...)`. @@ -355,16 +369,41 @@ impl EvalValuesHook { Self::Asin => one_arg(evaluated_args, values, eval_asin_result), Self::Atan => one_arg(evaluated_args, values, eval_atan_result), Self::Atan2 => two_args(evaluated_args, values, eval_atan2_result), - Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), + Self::BaseConvert => three_args(evaluated_args, values, eval_base_convert_result), + Self::Base64Decode => match evaluated_args { + [value] => eval_base64_decode_result(*value, false, values), + [value, strict] => { + let strict = values.truthy(*strict)?; + eval_base64_decode_result(*value, strict, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), Self::Boolval => one_arg(evaluated_args, values, eval_boolval_result), Self::Ceil => one_arg(evaluated_args, values, eval_ceil_result), Self::Chr => one_arg(evaluated_args, values, eval_chr_result), + Self::ChunkSplit => match evaluated_args { + [subject] => eval_chunk_split_result(*subject, None, None, values), + [subject, length] => { + eval_chunk_split_result(*subject, Some(*length), None, values) + } + [subject, length, separator] => { + eval_chunk_split_result(*subject, Some(*length), Some(*separator), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), Self::Core => eval_core_values_result(name, evaluated_args, context, values), Self::Cos => one_arg(evaluated_args, values, eval_cos_result), Self::Cosh => one_arg(evaluated_args, values, eval_cosh_result), + Self::CountChars => match evaluated_args { + [subject] => eval_count_chars_result(*subject, None, context, values), + [subject, mode] => { + eval_count_chars_result(*subject, Some(*mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), Self::Ctype => one_arg(evaluated_args, values, |value, values| match name { "ctype_alnum" => eval_ctype_alnum_result(value, values), @@ -382,7 +421,11 @@ impl EvalValuesHook { Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), Self::Hypot => two_args(evaluated_args, values, eval_hypot_result), Self::Floatval => one_arg(evaluated_args, values, eval_floatval_result), - Self::Intval => one_arg(evaluated_args, values, eval_intval_result), + Self::Intval => match evaluated_args { + [value] => eval_intval_result(*value, None, values), + [value, base] => eval_intval_result(*value, Some(*base), values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::IsArray => one_arg(evaluated_args, values, eval_is_array_result), Self::IsBool => one_arg(evaluated_args, values, eval_is_bool_result), Self::IsDouble => one_arg(evaluated_args, values, eval_is_double_result), @@ -495,12 +538,19 @@ impl EvalValuesHook { } Self::Printf => eval_printf_result(evaluated_args, values), Self::Pow => two_args(evaluated_args, values, eval_pow_result), + Self::QuoteMeta => one_arg(evaluated_args, values, eval_quotemeta_result), + Self::QuotedPrintableEncode => { + one_arg(evaluated_args, values, eval_quoted_printable_encode_result) + } Self::Rad2deg => one_arg(evaluated_args, values, eval_rad2deg_result), Self::Rand => eval_rand_values_result(evaluated_args, values), Self::RandomInt => eval_random_int_values_result(evaluated_args, values), Self::Round => match evaluated_args { - [value] => eval_round_result(*value, None, values), - [value, precision] => eval_round_result(*value, Some(*precision), values), + [value] => eval_round_result(*value, None, None, values), + [value, precision] => eval_round_result(*value, Some(*precision), None, values), + [value, precision, mode] => { + eval_round_result(*value, Some(*precision), Some(*mode), values) + } _ => Err(EvalStatus::RuntimeFatal), }, Self::MbEregMatch => eval_mb_ereg_match_values_result(evaluated_args, values), @@ -554,13 +604,20 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), } }), - Self::StringPosition => two_args(evaluated_args, values, |haystack, needle, values| { + Self::StringPosition => { + let (haystack, needle, offset) = match evaluated_args { + [haystack, needle] => (*haystack, *needle, None), + [haystack, needle, offset] => (*haystack, *needle, Some(*offset)), + _ => return Err(EvalStatus::RuntimeFatal), + }; match name { - "strpos" => eval_strpos_result(haystack, needle, values), - "strrpos" => eval_strrpos_result(haystack, needle, values), + "stripos" => eval_stripos_result(haystack, needle, offset, values), + "strpos" => eval_strpos_result(haystack, needle, offset, values), + "strripos" => eval_strripos_result(haystack, needle, offset, values), + "strrpos" => eval_strrpos_result(haystack, needle, offset, values), _ => Err(EvalStatus::RuntimeFatal), } - }), + } Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { match name { "str_contains" => eval_str_contains_result(haystack, needle, values), @@ -620,6 +677,20 @@ impl EvalValuesHook { [value, length] => eval_str_split_result(*value, Some(*length), values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::StrWordCount => match evaluated_args { + [subject] => eval_str_word_count_result(*subject, None, None, context, values), + [subject, format] => { + eval_str_word_count_result(*subject, Some(*format), None, context, values) + } + [subject, format, characters] => eval_str_word_count_result( + *subject, + Some(*format), + Some(*characters), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Strlen => match name { "mb_strlen" => match evaluated_args { [value] => eval_mb_strlen_result(*value, None, context, values), @@ -636,6 +707,11 @@ impl EvalValuesHook { eval_strval_result(value, context, values) }), Self::Strrev => one_arg(evaluated_args, values, eval_strrev_result), + Self::Strtr => match evaluated_args { + [subject, from] => eval_strtr_result(*subject, *from, None, values), + [subject, from, to] => eval_strtr_result(*subject, *from, Some(*to), values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Strstr => match evaluated_args { [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), [haystack, needle, before_needle] => { diff --git a/crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs b/crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs new file mode 100644 index 0000000000..a76a8aed6b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `base_convert`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Mirrors php-src's `_php_math_basetozval` + `_php_math_zvaltobase` pair, including the +//! widening to `double` past `PHP_INT_MAX` and the deliberately lossy float render that +//! makes `base_convert("ffffffffffffffff", 16, 10)` produce `"18446744073709552046"`. +//! - Characters that are not digits of `$from_base` are ignored rather than terminating the +//! scan, and a base outside `2..=36` is php-src's `ValueError`, reported as a runtime fatal. + +eval_builtin! { + name: "base_convert", + area: Math, + params: [num, from_base, to_base], + direct: BaseConvert, + values: BaseConvert, +} + +use super::super::super::*; + +/// Digit alphabet php-src uses for every base up to 36. +const BASE_DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz"; + +/// Largest digit count php-src's `_php_math_zvaltobase` float buffer can hold. +const MAX_FLOAT_DIGITS: usize = 64; + +/// Numeric value parsed out of a numeral string, widened exactly where php-src widens. +enum ParsedNumeral { + /// The value still fits `PHP_INT_MAX` and renders exactly. + Int(i64), + /// The value overflowed and renders through php-src's lossy float loop. + Float(f64), +} + +/// Evaluates PHP `base_convert(...)` over one numeral and its two base arguments. +pub(in crate::interpreter) fn eval_builtin_base_convert( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num, from_base, to_base] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + let from_base = eval_expr(from_base, context, scope, values)?; + let to_base = eval_expr(to_base, context, scope, values)?; + eval_base_convert_result(num, from_base, to_base, values) +} + +/// Re-renders an already evaluated numeral from one base into another. +pub(in crate::interpreter) fn eval_base_convert_result( + num: RuntimeCellHandle, + from_base: RuntimeCellHandle, + to_base: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(num)?; + let from_base = eval_int_value(from_base, values)?; + let to_base = eval_int_value(to_base, values)?; + if !(2..=36).contains(&from_base) || !(2..=36).contains(&to_base) { + return Err(EvalStatus::RuntimeFatal); + } + let parsed = eval_base_to_number(&bytes, from_base as u32); + let output = eval_number_to_base(parsed, to_base as u32); + values.string_bytes_value(&output) +} + +/// Parses `bytes` as a numeral in `base`, widening to `f64` exactly where php-src does. +fn eval_base_to_number(bytes: &[u8], base: u32) -> ParsedNumeral { + let base_i64 = i64::from(base); + let cutoff = i64::MAX / base_i64; + let cutlim = i64::MAX % base_i64; + let mut accumulator = 0i64; + let mut widened = 0f64; + let mut is_float = false; + for byte in bytes { + let digit = match byte { + b'0'..=b'9' => u32::from(byte - b'0'), + b'A'..=b'Z' => u32::from(byte - b'A') + 10, + b'a'..=b'z' => u32::from(byte - b'a') + 10, + _ => continue, + }; + if digit >= base { + continue; + } + let digit = i64::from(digit); + if is_float { + widened = widened * f64::from(base) + digit as f64; + continue; + } + if accumulator > cutoff || (accumulator == cutoff && digit > cutlim) { + is_float = true; + widened = accumulator as f64 * f64::from(base) + digit as f64; + continue; + } + accumulator = accumulator * base_i64 + digit; + } + if is_float { + ParsedNumeral::Float(widened) + } else { + ParsedNumeral::Int(accumulator) + } +} + +/// Renders a parsed numeral in `base`, reproducing php-src's exact and lossy paths. +fn eval_number_to_base(parsed: ParsedNumeral, base: u32) -> Vec { + let value = match parsed { + ParsedNumeral::Int(value) => { + let mut unsigned = value as u64; + if unsigned == 0 { + return b"0".to_vec(); + } + let mut digits = Vec::new(); + while unsigned != 0 { + digits.push(BASE_DIGITS[(unsigned % u64::from(base)) as usize]); + unsigned /= u64::from(base); + } + digits.reverse(); + return digits; + } + ParsedNumeral::Float(value) => value, + }; + + let mut running = value.floor(); + if running.is_infinite() { + return Vec::new(); + } + let divisor = f64::from(base); + let mut digits = Vec::new(); + loop { + let remainder = running % divisor; + digits.push(BASE_DIGITS[(remainder as i64).unsigned_abs() as usize % base as usize]); + running /= divisor; + if digits.len() >= MAX_FLOAT_DIGITS || running.abs() < 1.0 { + break; + } + } + digits.reverse(); + digits +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index ac4de3582a..be1d6b7b35 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -13,6 +13,7 @@ mod acos; mod asin; mod atan; mod atan2; +mod base_convert; mod ceil; mod clamp; mod cos; @@ -47,6 +48,7 @@ pub(in crate::interpreter) use acos::*; pub(in crate::interpreter) use asin::*; pub(in crate::interpreter) use atan::*; pub(in crate::interpreter) use atan2::*; +pub(in crate::interpreter) use base_convert::*; pub(in crate::interpreter) use ceil::*; pub(in crate::interpreter) use clamp::*; pub(in crate::interpreter) use cos::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/round.rs b/crates/elephc-magician/src/interpreter/builtins/math/round.rs index c93d7e02f0..d7ab4d4699 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/round.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/round.rs @@ -14,12 +14,16 @@ use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { name: "round", area: Math, - params: [num, precision = EvalBuiltinDefaultValue::Int(0)], + params: [ + num, + precision = EvalBuiltinDefaultValue::Int(0), + mode = EvalBuiltinDefaultValue::Int(EVAL_PHP_ROUND_HALF_UP) + ], direct: Round, values: Round, } -/// Evaluates PHP `round()` over one value and an optional precision expression. +/// Evaluates PHP `round()` over one value plus optional precision and mode expressions. pub(in crate::interpreter) fn eval_builtin_round( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -29,22 +33,126 @@ pub(in crate::interpreter) fn eval_builtin_round( match args { [num] => { let num = eval_expr(num, context, scope, values)?; - eval_round_result(num, None, values) + eval_round_result(num, None, None, values) } [num, precision] => { let num = eval_expr(num, context, scope, values)?; let precision = eval_expr(precision, context, scope, values)?; - eval_round_result(num, Some(precision), values) + eval_round_result(num, Some(precision), None, values) + } + [num, precision, mode] => { + let num = eval_expr(num, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_round_result(num, Some(precision), Some(mode), values) } _ => Err(EvalStatus::RuntimeFatal), } } /// Applies PHP `round()` to already evaluated arguments. +/// +/// The default half-away-from-zero mode delegates to the runtime rounding op. The +/// other three PHP modes differ only for exact `.5` ties, so they are resolved here +/// by scaling to the requested precision, detecting the tie against `floor + 0.5`, +/// and choosing the neighbour PHP's mode selects; everything that is not a tie falls +/// back to the same runtime op, which keeps ordinary values bit-identical. pub(in crate::interpreter) fn eval_round_result( num: RuntimeCellHandle, precision: Option, + mode: Option, values: &mut impl RuntimeValueOps, ) -> Result { + let Some(mode) = mode else { + return values.round(num, precision); + }; + for (tag, resolver) in [ + (EVAL_PHP_ROUND_HALF_DOWN, eval_round_half_down as RoundTieResolver), + (EVAL_PHP_ROUND_HALF_EVEN, eval_round_half_even), + (EVAL_PHP_ROUND_HALF_ODD, eval_round_half_odd), + ] { + let tag = values.int(tag)?; + let selected = values.compare(EvalBinOp::StrictEq, mode, tag)?; + if values.truthy(selected)? { + return eval_round_tie_aware(num, precision, resolver, values); + } + } values.round(num, precision) } + +/// Chooses between the two neighbours of an exact `.5` tie for one rounding mode. +type RoundTieResolver = fn( + lower: RuntimeCellHandle, + upper: RuntimeCellHandle, + values: &mut dyn RoundTieOps, +) -> Result; + +/// Rounds a tie toward zero, which for the scaled value means taking the lower neighbour. +fn eval_round_half_down( + lower: RuntimeCellHandle, + _upper: RuntimeCellHandle, + _values: &mut dyn RoundTieOps, +) -> Result { + Ok(lower) +} + +/// Rounds a tie to whichever neighbour is even. +fn eval_round_half_even( + lower: RuntimeCellHandle, + upper: RuntimeCellHandle, + values: &mut dyn RoundTieOps, +) -> Result { + if values.is_even(lower)? { Ok(lower) } else { Ok(upper) } +} + +/// Rounds a tie to whichever neighbour is odd. +fn eval_round_half_odd( + lower: RuntimeCellHandle, + upper: RuntimeCellHandle, + values: &mut dyn RoundTieOps, +) -> Result { + if values.is_even(lower)? { Ok(upper) } else { Ok(lower) } +} + +/// The parity probe the tie resolvers need, kept object-safe so they can be plain fn pointers. +pub(in crate::interpreter) trait RoundTieOps { + /// Returns whether a scaled integral cell is an even number. + fn is_even(&mut self, value: RuntimeCellHandle) -> Result; +} + +impl RoundTieOps for T { + fn is_even(&mut self, value: RuntimeCellHandle) -> Result { + let two = self.int(2)?; + let remainder = self.fmod(value, two)?; + let zero = self.int(0)?; + let equal = self.compare(EvalBinOp::LooseEq, remainder, zero)?; + self.truthy(equal) + } +} + +/// Applies a tie-aware rounding mode at the requested precision. +fn eval_round_tie_aware( + num: RuntimeCellHandle, + precision: Option, + resolver: RoundTieResolver, + values: &mut impl RuntimeValueOps, +) -> Result { + let ten = values.float(10.0)?; + let exponent = match precision { + Some(precision) => precision, + None => values.int(0)?, + }; + let scale = values.pow(ten, exponent)?; + let scaled = values.mul(num, scale)?; + let lower = values.floor(scaled)?; + let half = values.float(0.5)?; + let midpoint = values.add(lower, half)?; + let is_tie = values.compare(EvalBinOp::LooseEq, scaled, midpoint)?; + if !values.truthy(is_tie)? { + return values.round(num, precision); + } + let one = values.int(1)?; + let upper = values.add(lower, one)?; + let chosen = resolver(lower, upper, values)?; + values.div(chosen, scale) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index 94dde9d5cc..716f446ce6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -32,6 +32,9 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( eval_dynamic_array_push_unshift_call(name, evaluated_args, context, values)? } "array_splice" => eval_dynamic_array_splice_call(evaluated_args, context, values)?, + "end" | "next" | "prev" | "reset" => { + eval_dynamic_array_pointer_call(name, evaluated_args, context, values)? + } "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { eval_dynamic_array_sort_call(name, evaluated_args, context, values)? @@ -175,6 +178,23 @@ fn eval_dynamic_array_splice_call( Ok(Some(removed)) } +/// Evaluates a dynamic internal array pointer call against a writable array. +fn eval_dynamic_array_pointer_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["array"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + if array.ref_target.is_none() { + return Ok(None); + } + let (cursor, result) = eval_array_pointer_move(name, array.value, context, values)?; + context.set_array_cursor(array.value, cursor); + Ok(Some(result)) +} + /// Evaluates a dynamic standard array sort call against a writable array. fn eval_dynamic_array_sort_call( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs index fe28c54bc5..b7ed48315d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs @@ -33,13 +33,17 @@ use super::*; "array_walk", "arsort", "asort", + "end", "flock", "fsockopen", "krsort", "ksort", "natcasesort", "natsort", + "next", "pfsockopen", + "prev", + "reset", "rsort", "settype", "shuffle", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs index 0d5073d556..1d4748a2cb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs @@ -69,11 +69,11 @@ fn declared_builtin_registry_derives_filesystem_metadata() { assert_eq!( ); assert_eq!( eval_declared_builtin_param_names("file"), - Some(["filename"].as_slice()) + Some(["filename", "flags"].as_slice()) ); assert_eq!( eval_declared_builtin_param_names("file_get_contents"), - Some(["filename"].as_slice()) + Some(["filename", "use_include_path", "context", "offset", "length"].as_slice()) ); assert_eq!( eval_declared_builtin_param_names("file_put_contents"), diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs index d2326aba45..da2ddc4abf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs @@ -6,60 +6,121 @@ //! //! Key details: //! - Runtime dispatch is declared here and implemented through the existing Base64 decode hook. +//! - The decoder is a port of php-src's `php_base64_decode_impl`, so eval and AOT agree on the +//! awkward cases: embedded whitespace is skipped without rotating the quartet lane, unpadded +//! input still flushes its accumulated bytes, and a stray byte is dropped by the lax mode +//! but makes `$strict = true` return `false`. + +use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { name: "base64_decode", area: String, - params: [string], + params: [string, strict = EvalBuiltinDefaultValue::Bool(false)], direct: Base64Decode, values: Base64Decode, } use super::super::super::*; -/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +/// Evaluates PHP's `base64_decode(...)` over one subject expression and an optional strict flag. pub(in crate::interpreter) fn eval_builtin_base64_decode( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_decode_result(value, values) + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, false, values) + } + [value, strict] => { + let value = eval_expr(value, context, scope, values)?; + let strict = eval_expr(strict, context, scope, values)?; + let strict = values.truthy(strict)?; + eval_base64_decode_result(value, strict, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Converts one eval value through PHP string conversion and decodes Base64 bytes. +/// +/// Returns `false` instead of a string when `strict` is set and the input holds a byte outside +/// the Base64 alphabet, data after a padding character, a truncated final group, or an invalid +/// amount of padding — exactly the four `goto fail` paths in php-src's decoder. pub(in crate::interpreter) fn eval_base64_decode_result( value: RuntimeCellHandle, + strict: bool, values: &mut impl RuntimeValueOps, ) -> Result { let input = values.string_bytes(value)?; - let mut output = Vec::with_capacity((input.len() / 4) * 3); - let mut quartet = Vec::with_capacity(4); + let mut output: Vec = Vec::with_capacity((input.len() / 4) * 3); + // `accepted` is php-src's `i`: it counts only characters that entered the accumulator, so + // a skipped byte never rotates the quartet lane. `padding` is reset by an accepted + // character in the lax mode and rejected outright in the strict one. + let mut accepted: usize = 0; + let mut padding: usize = 0; for byte in input { - if byte.is_ascii_whitespace() { - continue; - } if byte == b'=' { - quartet.push(None); - } else if let Some(value) = eval_base64_decode_sextet(byte) { - quartet.push(Some(value)); - } else { + padding += 1; continue; } - if quartet.len() == 4 { - eval_push_base64_decoded_quartet(&quartet, &mut output); - quartet.clear(); + let sextet = match eval_base64_decode_sextet(byte) { + Some(sextet) => sextet, + None => { + if byte.is_ascii() && matches!(byte, b'\t' | b'\n' | 0x0C | b'\r' | b' ') { + continue; + } + if strict { + return values.bool_value(false); + } + continue; + } + }; + if padding > 0 { + if strict { + return values.bool_value(false); + } + padding = 0; + } + match accepted % 4 { + 0 => output.push(sextet << 2), + 1 => { + let last = output.len() - 1; + output[last] |= sextet >> 4; + output.push((sextet & 0x0f) << 4); + } + 2 => { + let last = output.len() - 1; + output[last] |= sextet >> 2; + output.push((sextet & 0x03) << 6); + } + _ => { + let last = output.len() - 1; + output[last] |= sextet; + } } + accepted += 1; } - if !quartet.is_empty() { - while quartet.len() < 4 { - quartet.push(None); + // php-src keeps the partially assembled trailing byte out of the result: `j` only advances + // when a byte is completed, so a group of 2 or 3 characters contributes 1 or 2 bytes. + let complete = accepted / 4 * 3 + + match accepted % 4 { + 0 => 0, + 1 => 0, + 2 => 1, + _ => 2, + }; + output.truncate(complete); + if strict { + if accepted % 4 == 1 { + return values.bool_value(false); + } + if padding > 0 && (padding > 2 || (accepted + padding) % 4 != 0) { + return values.bool_value(false); } - eval_push_base64_decoded_quartet(&quartet, &mut output); } values.string_bytes_value(&output) } @@ -75,22 +136,3 @@ pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option _ => None, } } - -/// Appends decoded bytes for one padded or unpadded Base64 quartet. -pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( - quartet: &[Option], - output: &mut Vec, -) { - let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { - return; - }; - output.push((first << 2) | (second >> 4)); - let Some(third) = quartet[2] else { - return; - }; - output.push(((second & 0x0f) << 4) | (third >> 2)); - let Some(fourth) = quartet[3] else { - return; - }; - output.push(((third & 0x03) << 6) | fourth); -} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs b/crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs new file mode 100644 index 0000000000..04a7f268b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `chunk_split`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Mirrors php-src exactly, including the back-compat branch that appends the separator +//! after the trailing partial chunk and returns a lone separator for an empty subject. +//! - A `$length` below 1 is php-src's `ValueError`, reported here as a runtime fatal. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "chunk_split", + area: String, + params: [ + string, + length = EvalBuiltinDefaultValue::Int(76), + separator = EvalBuiltinDefaultValue::String("\r\n"), + ], + direct: ChunkSplit, + values: ChunkSplit, +} + +use super::super::super::*; + +/// Evaluates PHP `chunk_split(...)` over one subject and its optional length/separator. +pub(in crate::interpreter) fn eval_builtin_chunk_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [subject] => { + let subject = eval_expr(subject, context, scope, values)?; + eval_chunk_split_result(subject, None, None, values) + } + [subject, length] => { + let subject = eval_expr(subject, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_chunk_split_result(subject, Some(length), None, values) + } + [subject, length, separator] => { + let subject = eval_expr(subject, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let separator = eval_expr(separator, context, scope, values)?; + eval_chunk_split_result(subject, Some(length), Some(separator), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits an already evaluated subject into fixed-size chunks joined by the separator. +pub(in crate::interpreter) fn eval_chunk_split_result( + subject: RuntimeCellHandle, + length: Option, + separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(subject)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 76, + }; + if length < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let separator = match separator { + Some(separator) => values.string_bytes(separator)?, + None => b"\r\n".to_vec(), + }; + let output = eval_chunk_split_bytes(&bytes, length as usize, &separator); + values.string_bytes_value(&output) +} + +/// Applies the php-src chunking rule over already converted byte slices. +/// +/// One pass always runs, so an empty subject yields exactly one separator — the observable +/// effect of php-src's `chunklen > srclen` back-compat branch. +pub(in crate::interpreter) fn eval_chunk_split_bytes( + bytes: &[u8], + length: usize, + separator: &[u8], +) -> Vec { + let mut output = Vec::with_capacity(bytes.len() + separator.len()); + let mut offset = 0usize; + loop { + let take = length.min(bytes.len() - offset); + output.extend_from_slice(&bytes[offset..offset + take]); + output.extend_from_slice(separator); + offset += take; + if offset >= bytes.len() { + break; + } + } + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs b/crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs new file mode 100644 index 0000000000..511b06affd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `count_chars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Mirrors php-src exactly: modes 0, 1, and 2 return byte-value keyed tallies (all bytes, +//! used bytes, unused bytes) and modes 3 and 4 return the used / unused byte values as a +//! string, always in ascending byte order. +//! - A mode outside `0..=4` is php-src's catchable `ValueError`, raised through eval's +//! pending-throw state so `catch (ValueError $e)` behaves as it does under the compiler. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "count_chars", + area: String, + params: [string, mode = EvalBuiltinDefaultValue::Int(0)], + direct: CountChars, + values: CountChars, +} + +use super::super::super::*; + +/// Evaluates PHP `count_chars(...)` over one subject and its optional mode. +pub(in crate::interpreter) fn eval_builtin_count_chars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [subject] => { + let subject = eval_expr(subject, context, scope, values)?; + eval_count_chars_result(subject, None, context, values) + } + [subject, mode] => { + let subject = eval_expr(subject, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_chars_result(subject, Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Tallies an already evaluated subject and materializes the requested `$mode` result. +pub(in crate::interpreter) fn eval_count_chars_result( + subject: RuntimeCellHandle, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(subject)?; + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => 0, + }; + if !(0..=4).contains(&mode) { + return eval_count_chars_mode_error(context, values); + } + let mut tally = [0i64; 256]; + for byte in bytes { + tally[byte as usize] += 1; + } + + if mode >= 3 { + let wanted_used = mode == 3; + let rendered = (0u32..256) + .filter(|index| (tally[*index as usize] != 0) == wanted_used) + .map(|index| index as u8) + .collect::>(); + return values.string_bytes_value(&rendered); + } + + // Modes 1 and 2 emit a sparse subset of the byte values, so the tally is built as an + // associative array: an indexed array would pad every skipped byte value with an empty + // element. Mode 0 uses the same shape so all three tally modes read identically. + let mut result = values.assoc_new(256)?; + for index in 0..256usize { + let count = tally[index]; + let keep = match mode { + 0 => true, + 1 => count != 0, + _ => count == 0, + }; + if !keep { + continue; + } + let key = values.int(index as i64)?; + let value = values.int(count)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Raises PHP's catchable `ValueError` for a `$mode` outside `0..=4`. +fn eval_count_chars_mode_error( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = + values.string("count_chars(): Argument #2 ($mode) must be between 0 and 4 (inclusive)")?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs index beac8a3f52..4ce66ca1d8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs @@ -6,7 +6,9 @@ //! //! Key details: //! - Direct and evaluated-argument dispatch stay in this leaf. -//! - The current eval implementation supports the two-argument runtime form. +//! - The optional `$limit` follows php-src: a positive limit caps the element count and lets +//! the last element absorb the remaining suffix, `0` behaves like `1`, and a negative limit +//! drops that many trailing segments. use super::super::spec::EvalBuiltinDefaultValue; @@ -20,41 +22,93 @@ eval_builtin! { use super::super::super::*; -/// Evaluates PHP `explode()` over separator and string expressions. +/// Evaluates PHP `explode()` over separator, string, and optional limit expressions. pub(in crate::interpreter) fn eval_builtin_explode( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [separator, string] = args else { - return Err(EvalStatus::RuntimeFatal); + let (separator, string, limit) = match args { + [separator, string] => (separator, string, None), + [separator, string, limit] => (separator, string, Some(limit)), + _ => return Err(EvalStatus::RuntimeFatal), }; let separator = eval_expr(separator, context, scope, values)?; let string = eval_expr(string, context, scope, values)?; - eval_explode_result(separator, string, values) + let limit = match limit { + Some(limit) => Some(eval_expr(limit, context, scope, values)?), + None => None, + }; + eval_explode_result(separator, string, limit, values) } /// Splits one PHP byte string into an indexed array using a non-empty separator. +/// +/// An omitted `$limit` means "no limit"; every other value follows php-src's rules, which are +/// resolved into a segment budget before any element is materialized. pub(in crate::interpreter) fn eval_explode_result( separator: RuntimeCellHandle, string: RuntimeCellHandle, + limit: Option, values: &mut impl RuntimeValueOps, ) -> Result { let separator = values.string_bytes(separator)?; if separator.is_empty() { return Err(EvalStatus::RuntimeFatal); } + let limit = match limit { + Some(limit) => eval_int_value(limit, values)?, + None => i64::MAX, + }; let string = values.string_bytes(string)?; + let segments = eval_explode_segments(&string, &separator); + let (cap, extend_last) = eval_explode_element_budget(limit, segments.len() as i64); let mut result = values.array_new(0)?; + if cap <= 0 { + return Ok(result); + } + for (index, (start, end)) in segments.iter().copied().enumerate() { + if index as i64 >= cap { + break; + } + let is_last_allowed = index as i64 + 1 == cap; + let end = if is_last_allowed && extend_last { + string.len() + } else { + end + }; + result = + eval_push_explode_segment(result, index as i64, &string[start..end], values)?; + } + Ok(result) +} + +/// Returns the `[start, end)` byte range of every segment a non-empty separator produces. +fn eval_explode_segments(string: &[u8], separator: &[u8]) -> Vec<(usize, usize)> { + let mut segments = Vec::new(); let mut start = 0; - let mut index = 0_i64; - while let Some(found) = super::strstr::eval_find_subslice(&string, &separator, start) { - result = eval_push_explode_segment(result, index, &string[start..found], values)?; + while let Some(found) = super::strstr::eval_find_subslice(string, separator, start) { + segments.push((start, found)); start = found + separator.len(); - index += 1; } - eval_push_explode_segment(result, index, &string[start..], values) + segments.push((start, string.len())); + segments +} + +/// Resolves PHP's `$limit` into an element budget plus whether the last element absorbs the tail. +/// +/// A positive limit caps the element count and lets the final element run to the end of the +/// subject, `0` is treated as `1`, and a negative limit keeps `total + limit` leading segments +/// with no tail absorption. +fn eval_explode_element_budget(limit: i64, total: i64) -> (i64, bool) { + if limit > 0 { + (limit, true) + } else if limit == 0 { + (1, true) + } else { + (total.saturating_add(limit), false) + } } /// Dispatches evaluated `explode()` calls through the builtin leaf. @@ -62,10 +116,13 @@ pub(in crate::interpreter) fn eval_explode_declared_values_result( evaluated_args: &[RuntimeCellHandle], values: &mut impl RuntimeValueOps, ) -> Result { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values) + match evaluated_args { + [separator, string] => eval_explode_result(*separator, *string, None, values), + [separator, string, limit] => { + eval_explode_result(*separator, *string, Some(*limit), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Appends one split segment to an indexed `explode()` result array. diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index 37669ba196..fa644ac540 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -15,6 +15,8 @@ mod base64_encode; mod bin2hex; mod chop; mod chr; +mod chunk_split; +mod count_chars; mod crc32; mod ctype_alnum; mod ctype_alpha; @@ -47,6 +49,8 @@ mod md5; mod nl2br; mod ord; mod parse_url; +mod quoted_printable_encode; +mod quotemeta; mod rawurldecode; mod rawurlencode; mod rtrim; @@ -59,6 +63,7 @@ mod str_repeat; mod str_replace; mod str_split; mod str_starts_with; +mod str_word_count; mod strcasecmp; mod strcmp; mod stream_get_filters; @@ -68,11 +73,14 @@ mod stream_is_local; mod stream_supports_lock; mod stripslashes; mod strlen; +mod stripos; mod strpos; mod strrev; +mod strripos; mod strrpos; mod strstr; mod strtolower; +mod strtr; mod strtoupper; mod substr; mod substr_replace; @@ -89,6 +97,8 @@ pub(in crate::interpreter) use base64_encode::*; pub(in crate::interpreter) use bin2hex::*; pub(in crate::interpreter) use chop::*; pub(in crate::interpreter) use chr::*; +pub(in crate::interpreter) use chunk_split::*; +pub(in crate::interpreter) use count_chars::*; pub(in crate::interpreter) use crc32::*; pub(in crate::interpreter) use ctype_alnum::*; pub(in crate::interpreter) use ctype_alpha::*; @@ -121,6 +131,8 @@ pub(in crate::interpreter) use md5::*; pub(in crate::interpreter) use nl2br::*; pub(in crate::interpreter) use ord::*; pub(in crate::interpreter) use parse_url::*; +pub(in crate::interpreter) use quoted_printable_encode::*; +pub(in crate::interpreter) use quotemeta::*; pub(in crate::interpreter) use rawurldecode::*; pub(in crate::interpreter) use rawurlencode::*; pub(in crate::interpreter) use rtrim::*; @@ -133,6 +145,7 @@ pub(in crate::interpreter) use str_repeat::*; pub(in crate::interpreter) use str_replace::*; pub(in crate::interpreter) use str_split::*; pub(in crate::interpreter) use str_starts_with::*; +pub(in crate::interpreter) use str_word_count::*; pub(in crate::interpreter) use strcasecmp::*; pub(in crate::interpreter) use strcmp::*; pub(in crate::interpreter) use stream_get_filters::*; @@ -142,11 +155,14 @@ pub(in crate::interpreter) use stream_is_local::*; pub(in crate::interpreter) use stream_supports_lock::*; pub(in crate::interpreter) use stripslashes::*; pub(in crate::interpreter) use strlen::*; +pub(in crate::interpreter) use stripos::*; pub(in crate::interpreter) use strpos::*; pub(in crate::interpreter) use strrev::*; +pub(in crate::interpreter) use strripos::*; pub(in crate::interpreter) use strrpos::*; pub(in crate::interpreter) use strstr::*; pub(in crate::interpreter) use strtolower::*; +pub(in crate::interpreter) use strtr::*; pub(in crate::interpreter) use strtoupper::*; pub(in crate::interpreter) use substr::*; pub(in crate::interpreter) use substr_replace::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs b/crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs new file mode 100644 index 0000000000..9198aa6c7f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs @@ -0,0 +1,107 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `quoted_printable_encode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Ports php-src's `php_quot_print_encode` byte-for-byte, including the pre-charged column +//! counter and the UTF-8 lookahead allowance that keeps a multi-byte character off a soft +//! line break, so eval and compiled output stay identical for binary input. + +eval_builtin! { + name: "quoted_printable_encode", + area: String, + params: [string], + direct: QuotedPrintableEncode, + values: QuotedPrintableEncode, +} + +use super::super::super::*; + +/// Maximum output column php-src allows before folding with a soft line break. +const QUOTED_PRINTABLE_MAX_LINE: usize = 75; + +/// Uppercase hex digits php-src writes after the `=` escape introducer. +const QUOTED_PRINTABLE_HEX: &[u8; 16] = b"0123456789ABCDEF"; + +/// Evaluates PHP's `quoted_printable_encode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_quoted_printable_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_quoted_printable_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and quoted-printable encodes it. +pub(in crate::interpreter) fn eval_quoted_printable_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string_bytes_value(&eval_quoted_printable_encode_bytes(&bytes)) +} + +/// Encodes `bytes` with the MIME quoted-printable transfer encoding php-src implements. +/// +/// An embedded `CRLF` is copied through and resets the column counter. A control byte, `0x7F`, +/// any high-bit byte, `=`, or a space directly before a `CR` becomes `=XX`; everything else is +/// literal. Lines are folded at column 75 with a trailing `=`, and a UTF-8 lead byte reserves +/// room for its continuation bytes so a character is never split across the fold. +pub(in crate::interpreter) fn eval_quoted_printable_encode_bytes(bytes: &[u8]) -> Vec { + let mut output: Vec = Vec::with_capacity(bytes.len()); + let mut column: usize = 0; + let mut index: usize = 0; + while index < bytes.len() { + let current = bytes[index]; + index += 1; + // php-src reads one byte past the current one and relies on the string's NUL + // terminator, so the lookahead past the final byte is zero rather than absent. + let lookahead = bytes.get(index).copied().unwrap_or(0); + if current == b'\r' && lookahead == b'\n' && index < bytes.len() { + output.push(b'\r'); + output.push(b'\n'); + index += 1; + column = 0; + continue; + } + let escaped = current < 0x20 + || current == 0x7F + || current & 0x80 != 0 + || current == b'=' + || (current == b' ' && lookahead == b'\r'); + if !escaped { + column += 1; + if column > QUOTED_PRINTABLE_MAX_LINE { + output.extend_from_slice(b"=\r\n"); + column = 1; + } + output.push(current); + continue; + } + column += 3; + let folds = match current { + 0x00..=0x7F => column > QUOTED_PRINTABLE_MAX_LINE, + 0x80..=0xDF => column + 3 > QUOTED_PRINTABLE_MAX_LINE, + 0xE0..=0xEF => column + 6 > QUOTED_PRINTABLE_MAX_LINE, + 0xF0..=0xF4 => column + 9 > QUOTED_PRINTABLE_MAX_LINE, + // Above 0xF4 no UTF-8 lead byte exists, and php-src's condition chain simply + // falls through without folding. Reproduced rather than "fixed". + _ => false, + }; + if folds { + output.extend_from_slice(b"=\r\n"); + column = 3; + } + output.push(b'='); + output.push(QUOTED_PRINTABLE_HEX[usize::from(current >> 4)]); + output.push(QUOTED_PRINTABLE_HEX[usize::from(current & 0x0F)]); + } + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs b/crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs new file mode 100644 index 0000000000..d4d0b70f0b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `quotemeta`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Escapes php-src's `quotemeta` character set verbatim, byte-for-byte, so eval and +//! compiled output stay identical for binary input. + +eval_builtin! { + name: "quotemeta", + area: String, + params: [string], + direct: QuoteMeta, + values: QuoteMeta, +} + +use super::super::super::*; + +/// Bytes PHP's `quotemeta` prefixes with a backslash. +const QUOTEMETA_ESCAPED: &[u8] = b".\\+*?[^]$()"; + +/// Evaluates PHP's `quotemeta(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_quotemeta( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_quotemeta_result(value, values) +} + +/// Converts one eval value through PHP string conversion and escapes its metacharacters. +pub(in crate::interpreter) fn eval_quotemeta_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string_bytes_value(&eval_quotemeta_bytes(&bytes)) +} + +/// Prefixes every regular-expression metacharacter in `bytes` with a single backslash. +pub(in crate::interpreter) fn eval_quotemeta_bytes(bytes: &[u8]) -> Vec { + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + if QUOTEMETA_ESCAPED.contains(byte) { + output.push(b'\\'); + } + output.push(*byte); + } + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs new file mode 100644 index 0000000000..2f842df44c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs @@ -0,0 +1,158 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `str_word_count`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Mirrors php-src's word definition exactly: C-locale `isalpha()` plus `'` and `-`, +//! extended by every byte of the optional `$characters` list, with a leading `'`/`-` and a +//! trailing `-` dropped unless the character list re-admits them. +//! - Format `0` returns the word count, `1` the list of words, and `2` the byte-offset map. +//! Any other format is php-src's catchable `ValueError`, raised through eval's +//! pending-throw state so `catch (ValueError $e)` behaves as it does under the compiler. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_word_count", + area: String, + params: [ + string, + format = EvalBuiltinDefaultValue::Int(0), + characters = EvalBuiltinDefaultValue::Null, + ], + direct: StrWordCount, + values: StrWordCount, +} + +use super::super::super::*; + +/// Evaluates PHP `str_word_count(...)` over one subject and its optional format/characters. +pub(in crate::interpreter) fn eval_builtin_str_word_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [subject] => { + let subject = eval_expr(subject, context, scope, values)?; + eval_str_word_count_result(subject, None, None, context, values) + } + [subject, format] => { + let subject = eval_expr(subject, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + eval_str_word_count_result(subject, Some(format), None, context, values) + } + [subject, format, characters] => { + let subject = eval_expr(subject, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + let characters = eval_expr(characters, context, scope, values)?; + eval_str_word_count_result(subject, Some(format), Some(characters), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Scans an already evaluated subject and materializes the requested `$format` result. +pub(in crate::interpreter) fn eval_str_word_count_result( + subject: RuntimeCellHandle, + format: Option, + characters: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(subject)?; + let format = match format { + Some(format) => eval_int_value(format, values)?, + None => 0, + }; + if !(0..=2).contains(&format) { + return eval_str_word_count_format_error(context, values); + } + let mut mask = [false; 256]; + if let Some(characters) = characters { + if !values.is_null(characters)? { + for byte in values.string_bytes(characters)? { + mask[byte as usize] = true; + } + } + } + + let words = str_word_count_words(&bytes, &mask); + if format == 0 { + let count = i64::try_from(words.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + return values.int(count); + } + // Format 2 keys words by their byte offset, which is sparse: an indexed array would pad + // every skipped offset with an empty element, so the map is built as an associative array. + let mut result = if format == 1 { + values.array_new(words.len())? + } else { + values.assoc_new(words.len())? + }; + for (index, (offset, word)) in words.iter().enumerate() { + let key = match format { + 1 => i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?, + _ => i64::try_from(*offset).map_err(|_| EvalStatus::RuntimeFatal)?, + }; + let key = values.int(key)?; + let value = values.string_bytes_value(word)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns each `(byte offset, word bytes)` pair php-src's `str_word_count()` would emit. +/// +/// The leading `'`/`-` and trailing `-` trims run before the scan, exactly as php-src does, +/// and a candidate that covers zero bytes is a separator rather than a word. +fn str_word_count_words(bytes: &[u8], mask: &[bool; 256]) -> Vec<(usize, Vec)> { + let mut words = Vec::new(); + if bytes.is_empty() { + return words; + } + let mut start = 0usize; + let mut end = bytes.len(); + if (bytes[0] == b'\'' && !mask[usize::from(b'\'')]) + || (bytes[0] == b'-' && !mask[usize::from(b'-')]) + { + start += 1; + } + if bytes[end - 1] == b'-' && !mask[usize::from(b'-')] { + end -= 1; + } + + let mut position = start; + while position < end { + let word_start = position; + while position < end && str_word_count_is_word_byte(bytes[position], mask) { + position += 1; + } + if position > word_start { + words.push((word_start, bytes[word_start..position].to_vec())); + } + position += 1; + } + words +} + +/// Returns whether one byte continues a php-src `str_word_count()` word. +fn str_word_count_is_word_byte(byte: u8, mask: &[bool; 256]) -> bool { + byte.is_ascii_alphabetic() || mask[usize::from(byte)] || byte == b'\'' || byte == b'-' +} + +/// Raises PHP's catchable `ValueError` for a `$format` outside `0..=2`. +fn eval_str_word_count_format_error( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = + values.string("str_word_count(): Argument #2 ($format) must be a valid format value")?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stripos.rs b/crates/elephc-magician/src/interpreter/builtins/string/stripos.rs new file mode 100644 index 0000000000..da34e6aed8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stripos.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stripos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the shared string-position hook, +//! which folds both operands with php-src's ASCII-only rule before the ordinary byte search. +//! - `$offset` follows `strpos()`: a negative value is resolved against the haystack length and +//! an offset outside the haystack is reference PHP's catchable `ValueError`, reported here as +//! `EvalStatus::RuntimeFatal` because eval has no throw machinery. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stripos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} + +use super::super::super::*; + +/// Evaluates PHP `stripos(...)` over haystack, needle, and optional offset expressions. +pub(in crate::interpreter) fn eval_builtin_stripos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("stripos", args, context, scope, values) +} + +/// Applies PHP `stripos(...)` to evaluated haystack, needle, and optional offset values. +pub(in crate::interpreter) fn eval_stripos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + offset: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("stripos", haystack, needle, offset, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs index ab68534750..67b81899a4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs @@ -6,6 +6,10 @@ //! //! Key details: //! - Runtime dispatch is declared here and implemented through the string-position hook. +//! - The shared hook also serves `strrpos`, and both accept PHP's optional `$offset`. An +//! offset outside the haystack is reference PHP's catchable `ValueError`; eval has no +//! throw machinery, so it reports `EvalStatus::RuntimeFatal` the way `str_repeat` does +//! for a negative count. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,7 +23,7 @@ eval_builtin! { use super::super::super::*; -/// Evaluates PHP `strpos(...)` over haystack and needle expressions. +/// Evaluates PHP `strpos(...)` over haystack, needle, and optional offset expressions. pub(in crate::interpreter) fn eval_builtin_strpos( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -29,13 +33,14 @@ pub(in crate::interpreter) fn eval_builtin_strpos( super::strpos::eval_builtin_string_position_named("strpos", args, context, scope, values) } -/// Applies PHP `strpos(...)` to evaluated haystack and needle values. +/// Applies PHP `strpos(...)` to evaluated haystack, needle, and optional offset values. pub(in crate::interpreter) fn eval_strpos_result( haystack: RuntimeCellHandle, needle: RuntimeCellHandle, + offset: Option, values: &mut impl RuntimeValueOps, ) -> Result { - super::strpos::eval_string_position_named_result("strpos", haystack, needle, values) + super::strpos::eval_string_position_named_result("strpos", haystack, needle, offset, values) } /// Evaluates one named PHP byte-string position builtin. @@ -46,39 +51,103 @@ pub(in crate::interpreter) fn eval_builtin_string_position_named( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); + let (haystack, needle, offset) = match args { + [haystack, needle] => (haystack, needle, None), + [haystack, needle, offset] => (haystack, needle, Some(offset)), + _ => return Err(EvalStatus::RuntimeFatal), }; let haystack = eval_expr(haystack, context, scope, values)?; let needle = eval_expr(needle, context, scope, values)?; - eval_string_position_named_result(name, haystack, needle, values) + let offset = match offset { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_string_position_named_result(name, haystack, needle, offset, values) } /// Returns the first or last byte offset of a converted needle, or PHP `false`. +/// +/// `offset` follows reference PHP: `strpos()` starts matching there, while a negative +/// `strrpos()` offset instead bounds where a match may end. Either spelling rejects an +/// offset outside the haystack, which PHP reports as a `ValueError`. pub(in crate::interpreter) fn eval_string_position_named_result( name: &str, haystack: RuntimeCellHandle, needle: RuntimeCellHandle, + offset: Option, values: &mut impl RuntimeValueOps, ) -> Result { let haystack = values.string_bytes(haystack)?; let needle = values.string_bytes(needle)?; + let offset = match offset { + Some(offset) => eval_int_value(offset, values)?, + None => 0, + }; + let window = string_position_window(name, &haystack, needle.len(), offset)?; + // `stripos`/`strripos` fold both operands with php-src's locale-independent ASCII rule, + // so a non-ASCII byte is still matched verbatim, then reuse the identical search. + let folded = matches!(name, "stripos" | "strripos"); + let haystack = if folded { + haystack.iter().map(u8::to_ascii_lowercase).collect() + } else { + haystack + }; + let needle = if folded { + needle.iter().map(u8::to_ascii_lowercase).collect() + } else { + needle + }; + let searched = &haystack[window.clone()]; let position = match name { - "strpos" if needle.is_empty() => Some(0), - "strpos" => haystack + "strpos" | "stripos" if needle.is_empty() => Some(0), + "strpos" | "stripos" => searched .windows(needle.len()) - .position(|window| window == needle), - "strrpos" if needle.is_empty() => Some(haystack.len()), - "strrpos" => haystack + .position(|candidate| candidate == needle), + "strrpos" | "strripos" if needle.is_empty() => Some(searched.len()), + "strrpos" | "strripos" => searched .windows(needle.len()) - .rposition(|window| window == needle), + .rposition(|candidate| candidate == needle), _ => return Err(EvalStatus::UnsupportedConstruct), }; match position { Some(position) => { - let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + let position = i64::try_from(position + window.start) + .map_err(|_| EvalStatus::RuntimeFatal)?; values.int(position) } None => values.bool_value(false), } } + +/// Resolves a `strpos()`-family `$offset` into the haystack byte range PHP actually scans. +/// +/// A `strpos()` offset (and a non-negative `strrpos()` one) simply moves the start of the +/// range; a negative `strrpos()` offset instead trims the end so no match may extend past +/// `strlen($haystack) + $offset + strlen($needle)`. An offset outside the haystack is +/// reported as `EvalStatus::RuntimeFatal`, eval's stand-in for PHP's `ValueError`. +fn string_position_window( + name: &str, + haystack: &[u8], + needle_len: usize, + offset: i64, +) -> Result, EvalStatus> { + let length = i64::try_from(haystack.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + if offset > length || offset < -length { + return Err(EvalStatus::RuntimeFatal); + } + if offset >= 0 { + let start = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(start..haystack.len()); + } + if matches!(name, "strpos" | "stripos") { + let start = usize::try_from(length + offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(start..haystack.len()); + } + let needle_len = i64::try_from(needle_len).map_err(|_| EvalStatus::RuntimeFatal)?; + if -offset < needle_len { + return Ok(0..haystack.len()); + } + let end = usize::try_from(length + offset + needle_len) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(0..end) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strripos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strripos.rs new file mode 100644 index 0000000000..aa2ea83332 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strripos.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `strripos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the shared string-position hook, +//! which folds both operands with php-src's ASCII-only rule before the ordinary byte search. +//! - `$offset` follows `strrpos()`: a negative value bounds where a match may END rather than +//! where the scan starts, and an offset outside the haystack is reference PHP's catchable +//! `ValueError`, reported here as `EvalStatus::RuntimeFatal`. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strripos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} + +use super::super::super::*; + +/// Evaluates PHP `strripos(...)` over haystack, needle, and optional offset expressions. +pub(in crate::interpreter) fn eval_builtin_strripos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("strripos", args, context, scope, values) +} + +/// Applies PHP `strripos(...)` to evaluated haystack, needle, and optional offset values. +pub(in crate::interpreter) fn eval_strripos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + offset: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("strripos", haystack, needle, offset, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs index 93b806a4db..61ccabeacb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs @@ -19,7 +19,7 @@ eval_builtin! { use super::super::super::*; -/// Evaluates PHP `strrpos(...)` over haystack and needle expressions. +/// Evaluates PHP `strrpos(...)` over haystack, needle, and optional offset expressions. pub(in crate::interpreter) fn eval_builtin_strrpos( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -29,11 +29,12 @@ pub(in crate::interpreter) fn eval_builtin_strrpos( super::strpos::eval_builtin_string_position_named("strrpos", args, context, scope, values) } -/// Applies PHP `strrpos(...)` to evaluated haystack and needle values. +/// Applies PHP `strrpos(...)` to evaluated haystack, needle, and optional offset values. pub(in crate::interpreter) fn eval_strrpos_result( haystack: RuntimeCellHandle, needle: RuntimeCellHandle, + offset: Option, values: &mut impl RuntimeValueOps, ) -> Result { - super::strpos::eval_string_position_named_result("strrpos", haystack, needle, values) + super::strpos::eval_string_position_named_result("strrpos", haystack, needle, offset, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtr.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtr.rs new file mode 100644 index 0000000000..5e6b4c4763 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtr.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Declarative eval registry entry and implementation for `strtr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Mirrors php-src's two shapes: `strtr($string, $from, $to)` translates bytes pairwise +//! (truncated to the shorter list, with a later pair for the same source byte winning), and +//! `strtr($string, $pairs)` applies replacement pairs longest-match-first in a single +//! left-to-right pass with no re-substitution. +//! - Keys are read through their PHP string spelling, so integer keys match the same +//! substrings php-src matches. Empty keys and keys longer than the whole subject are +//! ignored exactly as php-src ignores them. +//! - php-src also emits `Warning: strtr(): Ignoring replacement of empty string` for a +//! zero-length key; eval skips the key with the same observable result without warning, +//! matching the compiled backend. + +use std::collections::HashMap; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strtr", + area: String, + params: [string, from, to = EvalBuiltinDefaultValue::Null], + direct: Strtr, + values: Strtr, +} + +use super::super::super::*; + +/// Evaluates PHP `strtr(...)` in either its pairwise or replacement-pair shape. +pub(in crate::interpreter) fn eval_builtin_strtr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [subject, from] => { + let subject = eval_expr(subject, context, scope, values)?; + let from = eval_expr(from, context, scope, values)?; + eval_strtr_result(subject, from, None, values) + } + [subject, from, to] => { + let subject = eval_expr(subject, context, scope, values)?; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_strtr_result(subject, from, Some(to), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies `strtr()` to already evaluated arguments, choosing the shape from `$from`. +pub(in crate::interpreter) fn eval_strtr_result( + subject: RuntimeCellHandle, + from: RuntimeCellHandle, + to: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(subject)?; + if values.is_array_like(from)? { + let pairs = eval_strtr_pairs(from, bytes.len(), values)?; + let output = strtr_replace_pairs(&bytes, &pairs); + return values.string_bytes_value(&output); + } + let from = values.string_bytes(from)?; + let to = match to { + Some(to) if !values.is_null(to)? => values.string_bytes(to)?, + _ => Vec::new(), + }; + let output = strtr_translate_bytes(&bytes, &from, &to); + values.string_bytes_value(&output) +} + +/// Collects the usable replacement pairs from a `$pairs` array in insertion order. +/// +/// Keys are read through their PHP string spelling so integer keys match the substrings +/// php-src matches. Empty keys, and keys that cannot fit inside the subject at all, are +/// skipped just as php-src skips them. +fn eval_strtr_pairs( + from: RuntimeCellHandle, + subject_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result, Vec>, EvalStatus> { + let len = values.array_len(from)?; + let mut pairs = HashMap::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(from, position)?; + let value = values.array_get(from, key)?; + let key = values.string_bytes(key)?; + if key.is_empty() || key.len() > subject_len { + continue; + } + let value = values.string_bytes(value)?; + pairs.insert(key, value); + } + Ok(pairs) +} + +/// Applies php-src's longest-match-first single pass over the subject. +fn strtr_replace_pairs(bytes: &[u8], pairs: &HashMap, Vec>) -> Vec { + let Some(max_len) = pairs.keys().map(Vec::len).max() else { + return bytes.to_vec(); + }; + let min_len = pairs.keys().map(Vec::len).min().unwrap_or(max_len); + + let mut output = Vec::with_capacity(bytes.len()); + let mut position = 0usize; + while position < bytes.len() { + let remaining = bytes.len() - position; + let mut matched = None; + let mut length = max_len.min(remaining); + while length >= min_len { + if let Some(replacement) = pairs.get(&bytes[position..position + length]) { + matched = Some((length, replacement)); + break; + } + length -= 1; + } + match matched { + Some((length, replacement)) => { + output.extend_from_slice(replacement); + position += length; + } + None => { + output.push(bytes[position]); + position += 1; + } + } + } + output +} + +/// Applies php-src's pairwise byte translation, truncated to the shorter byte list. +fn strtr_translate_bytes(bytes: &[u8], from: &[u8], to: &[u8]) -> Vec { + let mut table = [0u8; 256]; + for (index, slot) in table.iter_mut().enumerate() { + *slot = index as u8; + } + for index in 0..from.len().min(to.len()) { + table[usize::from(from[index])] = to[index]; + } + bytes + .iter() + .map(|byte| table[usize::from(*byte)]) + .collect::>() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs index 6354b892e6..5dfe20a5fd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs @@ -7,35 +7,126 @@ //! Key details: //! - Cast behavior is implemented here; shared scalar coercions still flow //! through `RuntimeValueOps`. +//! - PHP's optional `$base` applies only to a string subject; every other type keeps the +//! plain `(int)` cast, which is why the base path is guarded by the cell's runtime tag. +//! - The base parser mirrors `strtol()` plus php-src's extra `0b` prefix, including its +//! `PHP_INT_MAX`/`PHP_INT_MIN` saturation and its `0` answer for an out-of-range base. use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use crate::interpreter::runtime_ops::EVAL_TAG_STRING; eval_builtin! { name: "intval", area: Types, - params: [value], + params: [value, base = EvalBuiltinDefaultValue::Int(10)], direct: Intval, values: Intval, } -/// Evaluates PHP `intval()` over one eval expression. +/// Evaluates PHP `intval()` over one eval expression and an optional base expression. pub(in crate::interpreter) fn eval_builtin_intval( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); + let (value, base) = match args { + [value] => (value, None), + [value, base] => (value, Some(base)), + _ => return Err(EvalStatus::RuntimeFatal), }; let value = eval_expr(value, context, scope, values)?; - eval_intval_result(value, values) + let base = match base { + Some(base) => Some(eval_expr(base, context, scope, values)?), + None => None, + }; + eval_intval_result(value, base, values) } -/// Applies PHP `intval()` to one already evaluated value. +/// Applies PHP `intval()` to one already evaluated value and optional base. +/// +/// An omitted base, a base of exactly `10`, and a non-string subject all reduce to the plain +/// `(int)` cast, exactly as php-src's `PHP_FUNCTION(intval)` short-circuits. pub(in crate::interpreter) fn eval_intval_result( value: RuntimeCellHandle, + base: Option, values: &mut impl RuntimeValueOps, ) -> Result { - values.cast_int(value) + let Some(base) = base else { + return values.cast_int(value); + }; + let base = eval_int_value(base, values)?; + if base == 10 || values.type_tag(value)? != EVAL_TAG_STRING { + return values.cast_int(value); + } + let bytes = values.string_bytes(value)?; + let parsed = eval_intval_parse_base(&bytes, base); + values.int(parsed) +} + +/// Parses one PHP byte string the way `strtol()` does for `intval($string, $base)`. +/// +/// Returns `0` for a base outside `0` and `2..=36`, saturates at `PHP_INT_MAX`/`PHP_INT_MIN` +/// instead of wrapping, and stops at the first byte that is not a digit of the resolved base. +fn eval_intval_parse_base(bytes: &[u8], base: i64) -> i64 { + if base != 0 && !(2..=36).contains(&base) { + return 0; + } + let mut rest = bytes; + while let [first, tail @ ..] = rest { + if *first == b' ' || (b'\t'..=b'\r').contains(first) { + rest = tail; + } else { + break; + } + } + let mut negative = false; + if let [first, tail @ ..] = rest { + if *first == b'-' || *first == b'+' { + negative = *first == b'-'; + rest = tail; + } + } + let mut base = base; + if rest.len() >= 2 && rest[0] == b'0' { + let marker = rest[1] | 0x20; + if marker == b'x' && (base == 0 || base == 16) { + base = 16; + rest = &rest[2..]; + } else if marker == b'b' && (base == 0 || base == 2) { + base = 2; + rest = &rest[2..]; + } + } + if base == 0 { + base = if rest.first() == Some(&b'0') { 8 } else { 10 }; + } + let limit: u64 = if negative { 1u64 << 63 } else { i64::MAX as u64 }; + let base = base as u64; + let mut accumulator: u64 = 0; + for byte in rest { + let Some(digit) = char::from(*byte).to_digit(36).map(u64::from) else { + break; + }; + if digit >= base { + break; + } + match accumulator + .checked_mul(base) + .and_then(|shifted| shifted.checked_add(digit)) + .filter(|candidate| *candidate <= limit) + { + Some(candidate) => accumulator = candidate, + None => { + accumulator = limit; + break; + } + } + } + if negative { + (accumulator as i64).wrapping_neg() + } else { + accumulator as i64 + } } diff --git a/crates/elephc-magician/src/interpreter/constant_eval.rs b/crates/elephc-magician/src/interpreter/constant_eval.rs index dcbb56f9ff..52dea9688d 100644 --- a/crates/elephc-magician/src/interpreter/constant_eval.rs +++ b/crates/elephc-magician/src/interpreter/constant_eval.rs @@ -99,8 +99,15 @@ pub(in crate::interpreter) fn eval_predefined_constant_value( "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), + "STR_PAD_LEFT" => Some(EvalPredefinedConstant::Int(EVAL_STR_PAD_LEFT)), + "STR_PAD_RIGHT" => Some(EvalPredefinedConstant::Int(EVAL_STR_PAD_RIGHT)), + "STR_PAD_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_STR_PAD_BOTH)), "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), + "PHP_ROUND_HALF_UP" => Some(EvalPredefinedConstant::Int(EVAL_PHP_ROUND_HALF_UP)), + "PHP_ROUND_HALF_DOWN" => Some(EvalPredefinedConstant::Int(EVAL_PHP_ROUND_HALF_DOWN)), + "PHP_ROUND_HALF_EVEN" => Some(EvalPredefinedConstant::Int(EVAL_PHP_ROUND_HALF_EVEN)), + "PHP_ROUND_HALF_ODD" => Some(EvalPredefinedConstant::Int(EVAL_PHP_ROUND_HALF_ODD)), "PREG_SPLIT_NO_EMPTY" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_NO_EMPTY)), "PREG_SPLIT_DELIM_CAPTURE" => { Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_DELIM_CAPTURE)) diff --git a/crates/elephc-magician/src/interpreter/constants.rs b/crates/elephc-magician/src/interpreter/constants.rs index caedf45285..91aabe7338 100644 --- a/crates/elephc-magician/src/interpreter/constants.rs +++ b/crates/elephc-magician/src/interpreter/constants.rs @@ -254,8 +254,22 @@ pub(super) const EVAL_LOCK_NB: i64 = 4; pub(super) const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; pub(super) const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; pub(super) const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; +/// `str_pad()` pads on the left of the input. +pub(super) const EVAL_STR_PAD_LEFT: i64 = 0; +/// `str_pad()` pads on the right of the input, which is PHP's default. +pub(super) const EVAL_STR_PAD_RIGHT: i64 = 1; +/// `str_pad()` splits the padding across both sides of the input. +pub(super) const EVAL_STR_PAD_BOTH: i64 = 2; pub(super) const EVAL_COUNT_NORMAL: i64 = 0; pub(super) const EVAL_COUNT_RECURSIVE: i64 = 1; +/// `round()` breaks exact `.5` ties away from zero, which is PHP's default. +pub(super) const EVAL_PHP_ROUND_HALF_UP: i64 = 1; +/// `round()` breaks exact `.5` ties toward zero. +pub(super) const EVAL_PHP_ROUND_HALF_DOWN: i64 = 2; +/// `round()` breaks exact `.5` ties toward the nearest even digit. +pub(super) const EVAL_PHP_ROUND_HALF_EVEN: i64 = 3; +/// `round()` breaks exact `.5` ties toward the nearest odd digit. +pub(super) const EVAL_PHP_ROUND_HALF_ODD: i64 = 4; pub(super) const EVAL_PREG_SPLIT_NO_EMPTY: i64 = 1; pub(super) const EVAL_PREG_SPLIT_DELIM_CAPTURE: i64 = 2; pub(super) const EVAL_PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; diff --git a/crates/elephc-magician/src/interpreter/expressions/calls.rs b/crates/elephc-magician/src/interpreter/expressions/calls.rs index fb05bc101c..2308a755a4 100644 --- a/crates/elephc-magician/src/interpreter/expressions/calls.rs +++ b/crates/elephc-magician/src/interpreter/expressions/calls.rs @@ -125,10 +125,14 @@ pub(in crate::interpreter) fn eval_call( | "array_walk" | "arsort" | "asort" + | "end" | "krsort" | "ksort" | "natcasesort" | "natsort" + | "next" + | "prev" + | "reset" | "rsort" | "shuffle" | "sort" diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 61ad04af78..f1df05fb60 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -33,9 +33,9 @@ mod output_handlers; mod throwables; use crate::context::{ - ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayReferenceKey, EvalReferenceTarget, - EvalClosure, EvalClosureCaptureBinding, EvalClosureObjectTarget, NativeCallableDefault, - NativeCallableSignature, NativeFunction, + ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayCursor, EvalArrayReferenceKey, + EvalReferenceTarget, EvalClosure, EvalClosureCaptureBinding, EvalClosureObjectTarget, + NativeCallableDefault, NativeCallableSignature, NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index a4a7f6b772..4ca4d54687 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -609,3 +609,104 @@ return function_exists("usort") && function_exists("uasort") && function_exists( ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval internal array pointer builtins read and move the cursor like PHP. +#[test] +fn execute_program_dispatches_array_pointer_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; +echo key($a) . ":" . current($a) . ":"; +echo next($a) . ":" . key($a) . ":"; +echo prev($a) . ":" . key($a) . ":"; +echo prev($a) . ":"; +echo (key($a) === null ? "NULL" : "K") . ":"; +echo (current($a) === false ? "FALSE" : "C") . ":"; +echo next($a) . ":"; +echo (key($a) === null ? "NULL" : "K") . ":"; +echo reset($a) . ":" . key($a) . ":"; +echo end($a) . ":" . key($a) . ":"; +echo next($a) . ":"; +echo (key($a) === null ? "NULL" : "K") . ":"; +echo prev($a) . ":"; +echo (key($a) === null ? "NULL" : "K") . ":"; +return function_exists("key") && function_exists("current") && function_exists("next") + && function_exists("prev") && function_exists("reset") && function_exists("end");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:1:2:1:1:0::NULL:FALSE::NULL:1:0:3:2::NULL::NULL:"); + assert!(values.warnings.is_empty()); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval internal array pointers survive foreach and follow assoc key order. +#[test] +fn execute_program_array_pointer_builtins_track_assoc_empty_and_replaced_arrays() { + let program = parse_fragment( + br#"$b = ["x" => 1, "y" => 2]; +echo key($b) . ":" . current($b) . ":"; +echo next($b) . ":" . key($b) . ":"; +foreach ($b as $k => $v) { echo $k . $v; } +echo ":" . key($b) . ":"; +echo current(array: $b) . ":" . key(array: $b) . ":"; +echo (next(array: $b) === false ? "FALSE" : "N") . ":" . (key($b) === null ? "NULL" : "K") . ":"; +$e = []; +echo (key($e) === null ? "NULL" : "K") . ":"; +echo (current($e) === false ? "FALSE" : "C") . ":"; +echo (reset($e) === false ? "FALSE" : "R") . ":"; +echo (end($e) === false ? "FALSE" : "E") . ":"; +echo (next($e) === false ? "FALSE" : "N") . ":"; +echo (prev($e) === false ? "FALSE" : "P") . ":"; +$c = [1, 2, 3]; +next($c); +$c = [4, 5, 6]; +echo key($c) . ":" . current($c) . ":"; +return function_exists("reset");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "x:1:2:y:x1y2:y:2:y:FALSE:NULL:NULL:FALSE:FALSE:FALSE:FALSE:FALSE:0:4:" + ); + assert!(values.warnings.is_empty()); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval internal array pointer movers warn and skip writeback on by-value calls. +#[test] +fn execute_program_array_pointer_builtins_warn_on_by_value_calls() { + let program = parse_fragment( + br#"$d = [1, 2, 3]; +next($d); +echo call_user_func("next", $d) . ":" . key($d) . ":"; +echo call_user_func("key", $d) . ":" . call_user_func("current", $d) . ":"; +echo call_user_func_array("reset", [$d]) . ":" . key($d) . ":"; +echo call_user_func("end", $d) . ":" . key($d) . ":"; +$f = "next"; +echo $f($d) . ":" . key($d) . ":"; +return function_exists("prev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:1:1:2:1:1:3:1:3:2:"); + assert_eq!( + values.warnings, + vec![ + "next(): Argument #1 ($array) must be passed by reference, value given", + "reset(): Argument #1 ($array) must be passed by reference, value given", + "end(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs index 2c18e4bcf8..1f5e70d5ec 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs @@ -272,3 +272,39 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval `file_get_contents()` honors PHP's `$offset`/`$length` window, its +/// unreachable-seek warning, and its negative-`$length` `ValueError`. +/// +/// The expected fragments are `LC_ALL=C php` 8.4 behavior for the same reads: a positive offset +/// seeks forward, a negative one counts from the end, a too-far negative one warns and answers +/// `false`, an offset past EOF answers `""`, and a `$length` past EOF is bounded by the file. +#[test] +fn execute_program_applies_file_get_contents_offset_and_length() { + let filename = format!("elephc_magician_fgc_range_{}.txt", std::process::id()); + let source = format!( + r#"file_put_contents("{filename}", "ABCDEFGHIJ"); +echo file_get_contents("{filename}", false, null, 3) . ":"; +echo file_get_contents("{filename}", false, null, 3, 4) . ":"; +echo file_get_contents("{filename}", false, null, -3) . ":"; +echo file_get_contents("{filename}", false, null, -3, 2) . ":"; +echo file_get_contents("{filename}", false, null, 0, 100) . ":"; +echo file_get_contents("{filename}", false, null, 20) === "" ? "past-eof" : "bad"; echo ":"; +echo file_get_contents("{filename}", false, null, 0, 0) === "" ? "zero" : "bad"; echo ":"; +echo file_get_contents("{filename}", true, null, 4, 3) . ":"; +echo file_get_contents("{filename}", false, null, -30) === false ? "seek-false" : "bad"; echo ":"; +echo unlink("{filename}") ? "unlinked" : "bad"; +return function_exists("file_get_contents");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "DEFGHIJ:DEFG:HIJ:HI:ABCDEFGHIJ:past-eof:zero:EFG:seek-false:unlinked" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/parser/cursor.rs b/crates/elephc-magician/src/parser/cursor.rs index ff11bc100d..509958a217 100644 --- a/crates/elephc-magician/src/parser/cursor.rs +++ b/crates/elephc-magician/src/parser/cursor.rs @@ -68,12 +68,36 @@ impl Parser { self.pos += 1; } } + + /// Returns true when the current token is the keyword `name`, compared case-insensitively. + pub(super) fn at_keyword(&self, name: &str) -> bool { + matches!(self.current(), TokenKind::Ident(actual) if ident_eq(actual, name)) + } + + /// Returns true when the current token is any of the keywords in `names`. + pub(super) fn at_any_keyword(&self, names: &[&str]) -> bool { + names.iter().any(|name| self.at_keyword(name)) + } + + /// Consumes the keyword `name` or returns a parse error. + pub(super) fn expect_keyword(&mut self, name: &str) -> Result<(), EvalParseError> { + if self.at_keyword(name) { + self.advance(); + Ok(()) + } else { + Err(EvalParseError::UnexpectedToken) + } + } } /// Returns true when the current token closes or starts a switch case arm. +/// +/// `endswitch` counts as a boundary so an alternative-syntax case body stops there instead of +/// trying to parse the terminator as a statement. pub(super) fn is_switch_case_boundary(token: &TokenKind) -> bool { matches!(token, TokenKind::RBrace) - || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) + || matches!(token, TokenKind::Ident(name) + if ident_eq(name, "case") || ident_eq(name, "default") || ident_eq(name, "endswitch")) } /// Maps simple variable assignment tokens to an optional compound EvalIR operator. diff --git a/crates/elephc-magician/src/parser/statements/control_flow.rs b/crates/elephc-magician/src/parser/statements/control_flow.rs index d04cf28312..70814e2b71 100644 --- a/crates/elephc-magician/src/parser/statements/control_flow.rs +++ b/crates/elephc-magician/src/parser/statements/control_flow.rs @@ -17,10 +17,16 @@ impl Parser { } /// Parses the condition, then block, and optional else branch for an `if` chain. + /// + /// Delegates to the alternative-syntax path when the body opens with `:`, which consumes the + /// whole `elseif:`/`else:` chain up to and including `endif;`. pub(in crate::parser) fn parse_if_after_keyword(&mut self) -> Result { self.expect(TokenKind::LParen)?; let condition = self.parse_expr()?; self.expect(TokenKind::RParen)?; + if matches!(self.current(), TokenKind::Colon) { + return self.parse_alternative_if_chain(condition); + } let then_branch = self.parse_statement_body()?; let else_branch = self.parse_optional_else_branch()?; Ok(EvalStmt::If { @@ -30,6 +36,86 @@ impl Parser { }) } + /// Parses a complete alternative-syntax `if` chain and consumes its closing `endif;`. + /// + /// `condition` is the already-parsed `if` condition and the cursor sits on the `:` that opens + /// the `then` segment. + fn parse_alternative_if_chain( + &mut self, + condition: EvalExpr, + ) -> Result { + let statement = self.parse_alternative_if_segment(condition)?; + self.expect_keyword("endif")?; + self.expect_semicolon()?; + Ok(statement) + } + + /// Parses one `: body` segment plus any `elseif:`/`else:` continuation, without consuming + /// the shared `endif;` that closes the whole chain. + /// + /// `elseif` recurses so the resulting `EvalStmt::If` nests exactly like the brace form. + fn parse_alternative_if_segment( + &mut self, + condition: EvalExpr, + ) -> Result { + self.expect(TokenKind::Colon)?; + let then_branch = self.parse_alternative_body(&["elseif", "else", "endif"])?; + let else_branch = if self.at_keyword("elseif") { + self.advance(); + self.expect(TokenKind::LParen)?; + let branch_condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + vec![self.parse_alternative_if_segment(branch_condition)?] + } else if self.at_keyword("else") { + self.advance(); + self.expect(TokenKind::Colon)?; + self.parse_alternative_body(&["endif"])? + } else { + Vec::new() + }; + Ok(EvalStmt::If { + condition, + then_branch, + else_branch, + }) + } + + /// Parses statements until one of the `stops` keywords, leaving that keyword unconsumed. + /// + /// Returns `UnexpectedEof` when the fragment ends before a terminator is found. + pub(in crate::parser) fn parse_alternative_body( + &mut self, + stops: &[&str], + ) -> Result, EvalParseError> { + let mut statements = Vec::new(); + loop { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if self.at_any_keyword(stops) { + break; + } + statements.extend(self.parse_nested_stmt()?); + } + Ok(statements) + } + + /// Parses a loop body in brace/braceless form, or in the alternative `:` … `;` + /// form when the body opens with `:`. + pub(in crate::parser) fn parse_statement_body_or_alternative( + &mut self, + terminator: &str, + ) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Colon) { + return self.parse_statement_body(); + } + self.advance(); + let body = self.parse_alternative_body(&[terminator])?; + self.expect_keyword(terminator)?; + self.expect_semicolon()?; + Ok(body) + } + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. pub(in crate::parser) fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { @@ -48,21 +134,41 @@ impl Parser { } } - /// Parses `switch (expr) { case expr: ... default: ... }`. + /// Parses `switch (expr) { case expr: ... default: ... }`, or the alternative + /// `switch (expr): case expr: ... endswitch;` form. pub(in crate::parser) fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LParen)?; let expr = self.parse_expr()?; self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; + // The case list is terminated by `}` in the brace form and by `endswitch` otherwise. + let alternative = matches!(self.current(), TokenKind::Colon); + if alternative { + self.advance(); + } else { + self.expect(TokenKind::LBrace)?; + } let mut cases = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { + loop { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } + let at_end = if alternative { + self.at_keyword("endswitch") + } else { + matches!(self.current(), TokenKind::RBrace) + }; + if at_end { + break; + } cases.push(self.parse_switch_case()?); } - self.expect(TokenKind::RBrace)?; + if alternative { + self.expect_keyword("endswitch")?; + self.expect_semicolon()?; + } else { + self.expect(TokenKind::RBrace)?; + } Ok(vec![EvalStmt::Switch { expr, cases }]) } @@ -152,13 +258,13 @@ impl Parser { Ok(statements) } - /// Parses `while (expr) { ... }`. + /// Parses `while (expr) { ... }`, or the alternative `while (expr): ... endwhile;` form. pub(in crate::parser) fn parse_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LParen)?; let condition = self.parse_expr()?; self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; + let body = self.parse_statement_body_or_alternative("endwhile")?; Ok(vec![EvalStmt::While { condition, body }]) } diff --git a/crates/elephc-magician/src/parser/statements/loops.rs b/crates/elephc-magician/src/parser/statements/loops.rs index 429c9856d4..69f60cb757 100644 --- a/crates/elephc-magician/src/parser/statements/loops.rs +++ b/crates/elephc-magician/src/parser/statements/loops.rs @@ -6,6 +6,8 @@ //! //! Key details: //! - Foreach key/value targets and statement bodies retain EvalIR source order. +//! - `for` and `foreach` accept PHP's alternative `:` … `endfor;`/`endforeach;` bodies, which +//! lower to the same EvalIR as the brace form. use super::*; @@ -59,7 +61,7 @@ impl Parser { }; self.expect_semicolon()?; let update = self.parse_for_update_clause()?; - let body = self.parse_statement_body()?; + let body = self.parse_statement_body_or_alternative("endfor")?; Ok(vec![EvalStmt::For { init, condition, @@ -95,7 +97,7 @@ impl Parser { (None, value_name) }; self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; + let body = self.parse_statement_body_or_alternative("endforeach")?; Ok(vec![EvalStmt::Foreach { array, key_name, diff --git a/crates/elephc-magician/src/parser/tests/control_statements.rs b/crates/elephc-magician/src/parser/tests/control_statements.rs index 359d1511f4..f7d706a79d 100644 --- a/crates/elephc-magician/src/parser/tests/control_statements.rs +++ b/crates/elephc-magician/src/parser/tests/control_statements.rs @@ -171,3 +171,94 @@ fn parse_fragment_accepts_foreach_key_value_source() { }] ); } + +// --- Alternative control-structure syntax --- + +/// Verifies an alternative-syntax `if`/`else` fragment lowers to the same branch statement as +/// the braced form, so a runtime `eval()` string behaves like the AOT-compiled one. +#[test] +fn parse_fragment_accepts_alternative_if_else_source() { + let alternative = + parse_fragment(br#"if ($flag): $x = "yes"; else: $x = "no"; endif;"#) + .expect("alternative fragment should parse"); + let braced = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies `elseif:` segments nest exactly like the braced `elseif` chain. +#[test] +fn parse_fragment_accepts_alternative_elseif_chain() { + let alternative = + parse_fragment(br#"if ($a): $x = 1; elseif ($b): $x = 2; else: $x = 3; endif;"#) + .expect("alternative fragment should parse"); + let braced = + parse_fragment(br#"if ($a) { $x = 1; } elseif ($b) { $x = 2; } else { $x = 3; }"#) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies the alternative `while` body lowers to the same loop as the braced form. +#[test] +fn parse_fragment_accepts_alternative_while_source() { + let alternative = parse_fragment(br#"while ($i): $i = $i - 1; endwhile;"#) + .expect("alternative fragment should parse"); + let braced = parse_fragment(br#"while ($i) { $i = $i - 1; }"#) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies the alternative `for` body lowers to the same loop as the braced form. +#[test] +fn parse_fragment_accepts_alternative_for_source() { + let alternative = parse_fragment(br#"for ($i = 0; $i < 3; $i++): $x = $i; endfor;"#) + .expect("alternative fragment should parse"); + let braced = parse_fragment(br#"for ($i = 0; $i < 3; $i++) { $x = $i; }"#) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies the alternative `foreach` body lowers to the same loop as the braced form, +/// including the `$key => $value` binding. +#[test] +fn parse_fragment_accepts_alternative_foreach_source() { + let alternative = parse_fragment(br#"foreach ($items as $k => $v): $x = $v; endforeach;"#) + .expect("alternative fragment should parse"); + let braced = parse_fragment(br#"foreach ($items as $k => $v) { $x = $v; }"#) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies the alternative `switch` case list lowers to the same arms as the braced form. +#[test] +fn parse_fragment_accepts_alternative_switch_source() { + let alternative = parse_fragment( + br#"switch ($x): case 1: $y = "one"; break; default: $y = "other"; endswitch;"#, + ) + .expect("alternative fragment should parse"); + let braced = parse_fragment( + br#"switch ($x) { case 1: $y = "one"; break; default: $y = "other"; }"#, + ) + .expect("braced fragment should parse"); + assert_eq!(alternative.statements(), braced.statements()); +} + +/// Verifies alternative bodies may be empty and that the two forms nest in either direction. +#[test] +fn parse_fragment_accepts_empty_and_nested_alternative_bodies() { + parse_fragment(br#"if ($a): endif;"#).expect("empty alternative if should parse"); + parse_fragment(br#"while ($a): endwhile;"#).expect("empty alternative while should parse"); + parse_fragment(br#"foreach ($a as $v): if ($v) { $x = 1; } endforeach;"#) + .expect("braced body nested in alternative loop should parse"); + parse_fragment(br#"foreach ($a as $v) { if ($v): $x = 1; endif; }"#) + .expect("alternative body nested in braced loop should parse"); +} + +/// Verifies an unterminated alternative body is rejected rather than silently consuming +/// the rest of the fragment. +#[test] +fn parse_fragment_rejects_unterminated_alternative_body() { + assert!(parse_fragment(br#"if ($a): $x = 1;"#).is_err()); + assert!(parse_fragment(br#"while ($a): $x = 1;"#).is_err()); + assert!(parse_fragment(br#"switch ($a): case 1: $x = 1;"#).is_err()); +} diff --git a/docs/beyond-php/buffers.md b/docs/beyond-php/buffers.md index 4fa28456d3..c96e24e1cb 100644 --- a/docs/beyond-php/buffers.md +++ b/docs/beyond-php/buffers.md @@ -70,6 +70,19 @@ Restrictions: Always enabled. Out-of-bounds aborts: `Fatal error: buffer index out of bounds` +## Length validation + +`buffer_new()` validates the requested length before allocating. A negative length, or a length +whose `length * stride` payload size does not fit in a machine word, aborts with: + +``` +Fatal error: buffer_new() length is negative or exceeds the maximum buffer size +``` + +This keeps the length recorded in the buffer header consistent with the memory the buffer actually +owns, so the bounds check above can never approve an index outside the allocation. A length that is +representable but larger than the configured heap still reports `Fatal error: heap memory exhausted`. + ## Memory layout ``` diff --git a/docs/compiling/linking-and-conditional-compilation.md b/docs/compiling/linking-and-conditional-compilation.md index 71cb5cce81..96f9c8f42a 100644 --- a/docs/compiling/linking-and-conditional-compilation.md +++ b/docs/compiling/linking-and-conditional-compilation.md @@ -181,6 +181,32 @@ mechanism: Shared libraries (`--emit cdylib`) keep the full runtime, since any exported symbol may be reached by a host the linker cannot see. +## Binary hardening + +Compiled binaries are hardened by default. There is no flag: the options below +are always applied and cannot be turned off. + +On **Linux**, every executable and shared library is linked with: + +| Option | Effect | +|---|---| +| `-z noexecstack` | Marks the stack non-executable (`PT_GNU_STACK` `RW`). elephc assembles its objects with `as`, which emits no `.note.GNU-stack` section, so without this GNU ld infers an **executable** stack and warns. Nothing elephc produces needs one: there is no JIT, and Fiber stacks are mapped read/write with a guard page. | +| `-z relro` | Maps the relocated head of the data segment read-only once startup relocation is done. | +| `-z now` | Resolves all relocations eagerly at load time, so `relro` can cover the GOT (full RELRO). | + +Whether the executable is also position-independent is decided by the system +toolchain, not by elephc: Linux executables are linked `-static` whenever the +program needs no dynamic library, and a driver configured with default-PIE (for +example Alpine/musl) turns that into a **static PIE**, while a driver without it +(for example Debian/Ubuntu glibc) produces a classic non-PIE static executable. +elephc does not force `-static-pie`, because it requires a libc built with +static-PIE support (`rcrt1.o`) that many distributions do not ship, and a +missing one is a hard link failure. + +On **macOS** these options do not apply: `ld64` does not accept `-z`, binaries +are position-independent by default, and the stack is non-executable at the +platform level. + ## Conditional compilation elephc supports compile-time feature branches with `ifdef`. Symbols are defined diff --git a/docs/internals/builtins/_internal/__elephc_callable_ptr.md b/docs/internals/builtins/_internal/__elephc_callable_ptr.md index 37e29bd430..887f737de0 100644 --- a/docs/internals/builtins/_internal/__elephc_callable_ptr.md +++ b/docs/internals/builtins/_internal/__elephc_callable_ptr.md @@ -2,7 +2,7 @@ title: "__elephc_callable_ptr() — internals" description: "Compiler internals for __elephc_callable_ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 461 + order: 488 --- ## `__elephc_callable_ptr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/elephc_callable_ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/elephc_callable_ptr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_class_has_constructor.md b/docs/internals/builtins/_internal/__elephc_class_has_constructor.md index 9c6d130282..d96846bc02 100644 --- a/docs/internals/builtins/_internal/__elephc_class_has_constructor.md +++ b/docs/internals/builtins/_internal/__elephc_class_has_constructor.md @@ -2,7 +2,7 @@ title: "__elephc_class_has_constructor() — internals" description: "Compiler internals for __elephc_class_has_constructor(): lowering path, type checks, and runtime helpers." sidebar: - order: 462 + order: 489 --- ## `__elephc_class_has_constructor()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_class_has_constructor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_class_has_constructor.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index fa15db0775..90e22e3dde 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_gmmktime_raw() — internals" description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 463 + order: 490 --- ## `__elephc_gmmktime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_gmmktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_gmmktime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_hash_ctx_copy.md b/docs/internals/builtins/_internal/__elephc_hash_ctx_copy.md index 030cd36013..5e766879b8 100644 --- a/docs/internals/builtins/_internal/__elephc_hash_ctx_copy.md +++ b/docs/internals/builtins/_internal/__elephc_hash_ctx_copy.md @@ -2,7 +2,7 @@ title: "__elephc_hash_ctx_copy() — internals" description: "Compiler internals for __elephc_hash_ctx_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 464 + order: 491 --- ## `__elephc_hash_ctx_copy()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/__elephc_hash_ctx_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/__elephc_hash_ctx_copy.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_hash_ctx_final.md b/docs/internals/builtins/_internal/__elephc_hash_ctx_final.md index 2cfb51f1f1..4edbdcd766 100644 --- a/docs/internals/builtins/_internal/__elephc_hash_ctx_final.md +++ b/docs/internals/builtins/_internal/__elephc_hash_ctx_final.md @@ -2,7 +2,7 @@ title: "__elephc_hash_ctx_final() — internals" description: "Compiler internals for __elephc_hash_ctx_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 465 + order: 492 --- ## `__elephc_hash_ctx_final()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/__elephc_hash_ctx_final.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/__elephc_hash_ctx_final.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_hash_ctx_init.md b/docs/internals/builtins/_internal/__elephc_hash_ctx_init.md index 52f9d784a5..2f1e81dcad 100644 --- a/docs/internals/builtins/_internal/__elephc_hash_ctx_init.md +++ b/docs/internals/builtins/_internal/__elephc_hash_ctx_init.md @@ -2,7 +2,7 @@ title: "__elephc_hash_ctx_init() — internals" description: "Compiler internals for __elephc_hash_ctx_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 466 + order: 493 --- ## `__elephc_hash_ctx_init()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/__elephc_hash_ctx_init.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/__elephc_hash_ctx_init.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_hash_ctx_update.md b/docs/internals/builtins/_internal/__elephc_hash_ctx_update.md index b88d53cac4..af36797ca1 100644 --- a/docs/internals/builtins/_internal/__elephc_hash_ctx_update.md +++ b/docs/internals/builtins/_internal/__elephc_hash_ctx_update.md @@ -2,7 +2,7 @@ title: "__elephc_hash_ctx_update() — internals" description: "Compiler internals for __elephc_hash_ctx_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 467 + order: 494 --- ## `__elephc_hash_ctx_update()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/__elephc_hash_ctx_update.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/__elephc_hash_ctx_update.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_initialize_pdo_statement.md b/docs/internals/builtins/_internal/__elephc_initialize_pdo_statement.md index 803376eb59..57279daa99 100644 --- a/docs/internals/builtins/_internal/__elephc_initialize_pdo_statement.md +++ b/docs/internals/builtins/_internal/__elephc_initialize_pdo_statement.md @@ -2,7 +2,7 @@ title: "__elephc_initialize_pdo_statement() — internals" description: "Compiler internals for __elephc_initialize_pdo_statement(): lowering path, type checks, and runtime helpers." sidebar: - order: 468 + order: 495 --- ## `__elephc_initialize_pdo_statement()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_initialize_pdo_statement.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_initialize_pdo_statement.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_invoke_pdo_statement_constructor.md b/docs/internals/builtins/_internal/__elephc_invoke_pdo_statement_constructor.md index 1dac19a3a5..fd8c57b111 100644 --- a/docs/internals/builtins/_internal/__elephc_invoke_pdo_statement_constructor.md +++ b/docs/internals/builtins/_internal/__elephc_invoke_pdo_statement_constructor.md @@ -2,7 +2,7 @@ title: "__elephc_invoke_pdo_statement_constructor() — internals" description: "Compiler internals for __elephc_invoke_pdo_statement_constructor(): lowering path, type checks, and runtime helpers." sidebar: - order: 469 + order: 496 --- ## `__elephc_invoke_pdo_statement_constructor()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_invoke_pdo_statement_constructor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_invoke_pdo_statement_constructor.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_mktime_raw.md b/docs/internals/builtins/_internal/__elephc_mktime_raw.md index bf24458d57..930252f059 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_mktime_raw() — internals" description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 470 + order: 497 --- ## `__elephc_mktime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_mktime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_mktime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_new_without_constructor.md b/docs/internals/builtins/_internal/__elephc_new_without_constructor.md index deb413694f..5921a50abb 100644 --- a/docs/internals/builtins/_internal/__elephc_new_without_constructor.md +++ b/docs/internals/builtins/_internal/__elephc_new_without_constructor.md @@ -2,7 +2,7 @@ title: "__elephc_new_without_constructor() — internals" description: "Compiler internals for __elephc_new_without_constructor(): lowering path, type checks, and runtime helpers." sidebar: - order: 471 + order: 498 --- ## `__elephc_new_without_constructor()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_new_without_constructor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_new_without_constructor.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_normalize_callable.md b/docs/internals/builtins/_internal/__elephc_normalize_callable.md index c62f15fab0..22b3084d3d 100644 --- a/docs/internals/builtins/_internal/__elephc_normalize_callable.md +++ b/docs/internals/builtins/_internal/__elephc_normalize_callable.md @@ -2,7 +2,7 @@ title: "__elephc_normalize_callable() — internals" description: "Compiler internals for __elephc_normalize_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 472 + order: 499 --- ## `__elephc_normalize_callable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/elephc_normalize_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/elephc_normalize_callable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_object_is_enum.md b/docs/internals/builtins/_internal/__elephc_object_is_enum.md new file mode 100644 index 0000000000..ee5cc90a48 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_object_is_enum.md @@ -0,0 +1,55 @@ +--- +title: "__elephc_object_is_enum() — internals" +description: "Compiler internals for __elephc_object_is_enum(): lowering path, type checks, and runtime helpers." +sidebar: + order: 500 +--- + +## `__elephc_object_is_enum()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/__elephc_object_is_enum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/__elephc_object_is_enum.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.__elephc_object_is_enum` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.__elephc_object_is_enum` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function __elephc_object_is_enum(mixed $value): 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_object_prop_count.md b/docs/internals/builtins/_internal/__elephc_object_prop_count.md new file mode 100644 index 0000000000..c4b20d324a --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_object_prop_count.md @@ -0,0 +1,55 @@ +--- +title: "__elephc_object_prop_count() — internals" +description: "Compiler internals for __elephc_object_prop_count(): lowering path, type checks, and runtime helpers." +sidebar: + order: 501 +--- + +## `__elephc_object_prop_count()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/__elephc_object_prop_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/__elephc_object_prop_count.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.__elephc_object_prop_count` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.__elephc_object_prop_count` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function __elephc_object_prop_count(mixed $value): int +``` + +## 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_object_prop_name.md b/docs/internals/builtins/_internal/__elephc_object_prop_name.md new file mode 100644 index 0000000000..656e43813f --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_object_prop_name.md @@ -0,0 +1,55 @@ +--- +title: "__elephc_object_prop_name() — internals" +description: "Compiler internals for __elephc_object_prop_name(): lowering path, type checks, and runtime helpers." +sidebar: + order: 502 +--- + +## `__elephc_object_prop_name()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/__elephc_object_prop_name.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/__elephc_object_prop_name.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.__elephc_object_prop_name` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.__elephc_object_prop_name` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function __elephc_object_prop_name(mixed $value, int $index): 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_object_prop_value.md b/docs/internals/builtins/_internal/__elephc_object_prop_value.md new file mode 100644 index 0000000000..28f35dee05 --- /dev/null +++ b/docs/internals/builtins/_internal/__elephc_object_prop_value.md @@ -0,0 +1,55 @@ +--- +title: "__elephc_object_prop_value() — internals" +description: "Compiler internals for __elephc_object_prop_value(): lowering path, type checks, and runtime helpers." +sidebar: + order: 503 +--- + +## `__elephc_object_prop_value()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/callables/__elephc_object_prop_value.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/__elephc_object_prop_value.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.__elephc_object_prop_value` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `fresh` +- **Effects**: `static (2 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.__elephc_object_prop_value` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function __elephc_object_prop_value(mixed $value, int $index): mixed +``` + +## 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_pdo_adapter_addr.md b/docs/internals/builtins/_internal/__elephc_pdo_adapter_addr.md index b1e4d8f1ee..5eb32853a8 100644 --- a/docs/internals/builtins/_internal/__elephc_pdo_adapter_addr.md +++ b/docs/internals/builtins/_internal/__elephc_pdo_adapter_addr.md @@ -2,7 +2,7 @@ title: "__elephc_pdo_adapter_addr() — internals" description: "Compiler internals for __elephc_pdo_adapter_addr(): lowering path, type checks, and runtime helpers." sidebar: - order: 473 + order: 504 --- ## `__elephc_pdo_adapter_addr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/elephc_pdo_adapter_addr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/elephc_pdo_adapter_addr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_pdo_called_class_status.md b/docs/internals/builtins/_internal/__elephc_pdo_called_class_status.md index e72caa848f..a47aa1488c 100644 --- a/docs/internals/builtins/_internal/__elephc_pdo_called_class_status.md +++ b/docs/internals/builtins/_internal/__elephc_pdo_called_class_status.md @@ -2,7 +2,7 @@ title: "__elephc_pdo_called_class_status() — internals" description: "Compiler internals for __elephc_pdo_called_class_status(): lowering path, type checks, and runtime helpers." sidebar: - order: 474 + order: 505 --- ## `__elephc_pdo_called_class_status()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_pdo_called_class_status.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_pdo_called_class_status.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_pdo_statement_class_status.md b/docs/internals/builtins/_internal/__elephc_pdo_statement_class_status.md index 91e195dc66..c46cecdc24 100644 --- a/docs/internals/builtins/_internal/__elephc_pdo_statement_class_status.md +++ b/docs/internals/builtins/_internal/__elephc_pdo_statement_class_status.md @@ -2,7 +2,7 @@ title: "__elephc_pdo_statement_class_status() — internals" description: "Compiler internals for __elephc_pdo_statement_class_status(): lowering path, type checks, and runtime helpers." sidebar: - order: 475 + order: 506 --- ## `__elephc_pdo_statement_class_status()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_pdo_statement_class_status.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_pdo_statement_class_status.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md index a7dc5c6db0..7aff074a5d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_bzip2_archive() — internals" description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 476 + order: 507 --- ## `__elephc_phar_bzip2_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_bzip2_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_bzip2_archive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md index 5ee47cbbfc..458c3a3977 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_decompress_archive() — internals" description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 477 + order: 508 --- ## `__elephc_phar_decompress_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_decompress_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_decompress_archive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md index 35f802e9ba..1ccde5f8b5 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_file_metadata() — internals" description: "Compiler internals for __elephc_phar_get_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 478 + order: 509 --- ## `__elephc_phar_get_file_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_get_file_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_file_metadata.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md index b4aadcf213..eb0b7f484f 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_metadata() — internals" description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 479 + order: 510 --- ## `__elephc_phar_get_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_get_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_metadata.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md index 888d170776..4d53f768c4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_hash() — internals" description: "Compiler internals for __elephc_phar_get_signature_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 480 + order: 511 --- ## `__elephc_phar_get_signature_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_get_signature_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_signature_hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md index 704947bcbb..ee92528b45 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_type() — internals" description: "Compiler internals for __elephc_phar_get_signature_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 481 + order: 512 --- ## `__elephc_phar_get_signature_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_get_signature_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_signature_type.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md index 72690aa0c7..6e5ad7c6a6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_stub() — internals" description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 482 + order: 513 --- ## `__elephc_phar_get_stub()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_get_stub.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_get_stub.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md index 51d66b18b8..c37a7b5ea4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_gzip_archive() — internals" description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 483 + order: 514 --- ## `__elephc_phar_gzip_archive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_gzip_archive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_gzip_archive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md index 8b0bb00a4d..c409fb5c40 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,7 +2,7 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 484 + order: 515 --- ## `__elephc_phar_list_entries()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_list_entries.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_list_entries.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index e111b235dc..f086ec414a 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 485 + order: 516 --- ## `__elephc_phar_set_compression()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_set_compression.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_compression.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md index 9d108a6bf5..35777b6255 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_file_metadata() — internals" description: "Compiler internals for __elephc_phar_set_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 486 + order: 517 --- ## `__elephc_phar_set_file_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_set_file_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_file_metadata.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md index a7502509e8..0e3674e6d6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_metadata() — internals" description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 487 + order: 518 --- ## `__elephc_phar_set_metadata()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_set_metadata.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_metadata.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md index 2e2133709e..03606ac5a8 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_stub() — internals" description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 488 + order: 519 --- ## `__elephc_phar_set_stub()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_set_stub.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_stub.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md index e4bb666edc..472092a2c5 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_zip_password() — internals" description: "Compiler internals for __elephc_phar_set_zip_password(): lowering path, type checks, and runtime helpers." sidebar: - order: 489 + order: 520 --- ## `__elephc_phar_set_zip_password()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_set_zip_password.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_set_zip_password.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md index 86cd4fe61b..4b7e9f00be 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_hash() — internals" description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 490 + order: 521 --- ## `__elephc_phar_sign_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_sign_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_sign_hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md index 30508cf192..71d3aec69f 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_openssl() — internals" description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." sidebar: - order: 491 + order: 522 --- ## `__elephc_phar_sign_openssl()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/__elephc_phar_sign_openssl.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/__elephc_phar_sign_openssl.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_is_null.md b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md index c5c819c104..dc0fcc171c 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_is_null.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_is_null.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_is_null() — internals" description: "Compiler internals for __elephc_ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 492 + order: 523 --- ## `__elephc_ptr_is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_read_string.md b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md index 45b02c0d41..a87f3e4444 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_read_string.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_read_string.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_read_string() — internals" description: "Compiler internals for __elephc_ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 493 + order: 524 --- ## `__elephc_ptr_read_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_read_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_ptr_write_string.md b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md index eff0c3c0d2..c60efe535e 100644 --- a/docs/internals/builtins/_internal/__elephc_ptr_write_string.md +++ b/docs/internals/builtins/_internal/__elephc_ptr_write_string.md @@ -2,7 +2,7 @@ title: "__elephc_ptr_write_string() — internals" description: "Compiler internals for __elephc_ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 494 + order: 525 --- ## `__elephc_ptr_write_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/__elephc_ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/__elephc_ptr_write_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index c1333b8a16..043f111faa 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: 495 + order: 526 --- ## `__elephc_strtotime_raw()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/__elephc_strtotime_raw.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/__elephc_strtotime_raw.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_all.md b/docs/internals/builtins/array/array_all.md index 28d8e2322e..365a6959dd 100644 --- a/docs/internals/builtins/array/array_all.md +++ b/docs/internals/builtins/array/array_all.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_all.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_any.md b/docs/internals/builtins/array/array_any.md index 9e1d74988c..497916d731 100644 --- a/docs/internals/builtins/array/array_any.md +++ b/docs/internals/builtins/array/array_any.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_any.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_any.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_chunk.md b/docs/internals/builtins/array/array_chunk.md index 73094b18af..a813db65aa 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_chunk.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function array_chunk(array $array, int $length): array +function array_chunk(array $array, int $length, bool $preserve_keys = false): array ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes 2–3 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/array/array_column.md b/docs/internals/builtins/array/array_column.md index 877cfd5201..6e951d7eb9 100644 --- a/docs/internals/builtins/array/array_column.md +++ b/docs/internals/builtins/array/array_column.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_column.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_combine.md b/docs/internals/builtins/array/array_combine.md index d1dd747cc8..c29dfa372d 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_combine.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_count_values.md b/docs/internals/builtins/array/array_count_values.md new file mode 100644 index 0000000000..a0c38fa9a5 --- /dev/null +++ b/docs/internals/builtins/array/array_count_values.md @@ -0,0 +1,56 @@ +--- +title: "array_count_values() — internals" +description: "Compiler internals for array_count_values(): lowering path, type checks, and runtime helpers." +sidebar: + order: 6 +--- + +## `array_count_values()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/array_count_values.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_count_values.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_count_values` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_count_values` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function array_count_values(array $array): array +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `array_count_values()`](../../../php/builtins/array/array_count_values.md) diff --git a/docs/internals/builtins/array/array_diff.md b/docs/internals/builtins/array/array_diff.md index 02a517cc54..0a68fc8d82 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -2,7 +2,7 @@ title: "array_diff() — internals" description: "Compiler internals for array_diff(): lowering path, type checks, and runtime helpers." sidebar: - order: 6 + order: 7 --- ## `array_diff()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_diff_assoc.md b/docs/internals/builtins/array/array_diff_assoc.md index 471328604d..8fe9044ee8 100644 --- a/docs/internals/builtins/array/array_diff_assoc.md +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -2,7 +2,7 @@ title: "array_diff_assoc() — internals" description: "Compiler internals for array_diff_assoc(): lowering path, type checks, and runtime helpers." sidebar: - order: 7 + order: 8 --- ## `array_diff_assoc()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_assoc.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_diff_key.md b/docs/internals/builtins/array/array_diff_key.md index 5b9338c2ba..cea4d475da 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -2,7 +2,7 @@ title: "array_diff_key() — internals" description: "Compiler internals for array_diff_key(): lowering path, type checks, and runtime helpers." sidebar: - order: 8 + order: 9 --- ## `array_diff_key()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_diff_key.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_fill.md b/docs/internals/builtins/array/array_fill.md index 2c177018e3..46bb62ff21 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -2,7 +2,7 @@ title: "array_fill() — internals" description: "Compiler internals for array_fill(): lowering path, type checks, and runtime helpers." sidebar: - order: 9 + order: 10 --- ## `array_fill()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/array/array_fill_keys.md b/docs/internals/builtins/array/array_fill_keys.md index a594ef0291..1d04ec8a81 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -2,7 +2,7 @@ title: "array_fill_keys() — internals" description: "Compiler internals for array_fill_keys(): lowering path, type checks, and runtime helpers." sidebar: - order: 10 + order: 11 --- ## `array_fill_keys()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_fill_keys.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_filter.md b/docs/internals/builtins/array/array_filter.md index bc5196d040..4a7615bbf2 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -2,7 +2,7 @@ title: "array_filter() — internals" description: "Compiler internals for array_filter(): lowering path, type checks, and runtime helpers." sidebar: - order: 11 + order: 12 --- ## `array_filter()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_filter.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_find.md b/docs/internals/builtins/array/array_find.md index e13080a6ec..7c40e49449 100644 --- a/docs/internals/builtins/array/array_find.md +++ b/docs/internals/builtins/array/array_find.md @@ -2,7 +2,7 @@ title: "array_find() — internals" description: "Compiler internals for array_find(): lowering path, type checks, and runtime helpers." sidebar: - order: 12 + order: 13 --- ## `array_find()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_find.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_find.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_flip.md b/docs/internals/builtins/array/array_flip.md index dcea2fc417..c4c6d84a64 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -2,7 +2,7 @@ title: "array_flip() — internals" description: "Compiler internals for array_flip(): lowering path, type checks, and runtime helpers." sidebar: - order: 13 + order: 14 --- ## `array_flip()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_flip.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect.md b/docs/internals/builtins/array/array_intersect.md index e0a913e442..271e78ad0b 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -2,7 +2,7 @@ title: "array_intersect() — internals" description: "Compiler internals for array_intersect(): lowering path, type checks, and runtime helpers." sidebar: - order: 14 + order: 15 --- ## `array_intersect()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect_assoc.md b/docs/internals/builtins/array/array_intersect_assoc.md index 48b3c478bc..e5df75167e 100644 --- a/docs/internals/builtins/array/array_intersect_assoc.md +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -2,7 +2,7 @@ title: "array_intersect_assoc() — internals" description: "Compiler internals for array_intersect_assoc(): lowering path, type checks, and runtime helpers." sidebar: - order: 15 + order: 16 --- ## `array_intersect_assoc()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect_assoc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_assoc.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_intersect_key.md b/docs/internals/builtins/array/array_intersect_key.md index 85dc0801f0..09dbd4a7ae 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -2,7 +2,7 @@ title: "array_intersect_key() — internals" description: "Compiler internals for array_intersect_key(): lowering path, type checks, and runtime helpers." sidebar: - order: 16 + order: 17 --- ## `array_intersect_key()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_intersect_key.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_is_list.md b/docs/internals/builtins/array/array_is_list.md index 2ce5d4b8cf..dcfe9b50b3 100644 --- a/docs/internals/builtins/array/array_is_list.md +++ b/docs/internals/builtins/array/array_is_list.md @@ -2,7 +2,7 @@ title: "array_is_list() — internals" description: "Compiler internals for array_is_list(): lowering path, type checks, and runtime helpers." sidebar: - order: 17 + order: 18 --- ## `array_is_list()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_is_list.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_is_list.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_exists.md b/docs/internals/builtins/array/array_key_exists.md index d1806bf9c1..2b8e7ca299 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -2,7 +2,7 @@ title: "array_key_exists() — internals" description: "Compiler internals for array_key_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 18 + order: 19 --- ## `array_key_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_first.md b/docs/internals/builtins/array/array_key_first.md index b1ec825efd..ef6314ed0d 100644 --- a/docs/internals/builtins/array/array_key_first.md +++ b/docs/internals/builtins/array/array_key_first.md @@ -2,7 +2,7 @@ title: "array_key_first() — internals" description: "Compiler internals for array_key_first(): lowering path, type checks, and runtime helpers." sidebar: - order: 19 + order: 20 --- ## `array_key_first()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_first.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_first.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_key_last.md b/docs/internals/builtins/array/array_key_last.md index e4e1e3dba1..c9f650fdec 100644 --- a/docs/internals/builtins/array/array_key_last.md +++ b/docs/internals/builtins/array/array_key_last.md @@ -2,7 +2,7 @@ title: "array_key_last() — internals" description: "Compiler internals for array_key_last(): lowering path, type checks, and runtime helpers." sidebar: - order: 20 + order: 21 --- ## `array_key_last()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_key_last.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_key_last.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_keys.md b/docs/internals/builtins/array/array_keys.md index 5ae6a62a0b..c1e0cb5505 100644 --- a/docs/internals/builtins/array/array_keys.md +++ b/docs/internals/builtins/array/array_keys.md @@ -2,7 +2,7 @@ title: "array_keys() — internals" description: "Compiler internals for array_keys(): lowering path, type checks, and runtime helpers." sidebar: - order: 21 + order: 22 --- ## `array_keys()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_keys.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_map.md b/docs/internals/builtins/array/array_map.md index 9dd3019e64..ebf2a9f40f 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -2,7 +2,7 @@ title: "array_map() — internals" description: "Compiler internals for array_map(): lowering path, type checks, and runtime helpers." sidebar: - order: 22 + order: 23 --- ## `array_map()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_map.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_merge.md b/docs/internals/builtins/array/array_merge.md index d270cac156..fd35a97666 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -2,7 +2,7 @@ title: "array_merge() — internals" description: "Compiler internals for array_merge(): lowering path, type checks, and runtime helpers." sidebar: - order: 23 + order: 24 --- ## `array_merge()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_merge_recursive.md b/docs/internals/builtins/array/array_merge_recursive.md index d3f7ff0e7a..a7d815ca6b 100644 --- a/docs/internals/builtins/array/array_merge_recursive.md +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -2,7 +2,7 @@ title: "array_merge_recursive() — internals" description: "Compiler internals for array_merge_recursive(): lowering path, type checks, and runtime helpers." sidebar: - order: 24 + order: 25 --- ## `array_merge_recursive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_merge_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_merge_recursive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_multisort.md b/docs/internals/builtins/array/array_multisort.md index 4e494bc183..a8f0c071f7 100644 --- a/docs/internals/builtins/array/array_multisort.md +++ b/docs/internals/builtins/array/array_multisort.md @@ -2,7 +2,7 @@ title: "array_multisort() — internals" description: "Compiler internals for array_multisort(): lowering path, type checks, and runtime helpers." sidebar: - order: 25 + order: 26 --- ## `array_multisort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_multisort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_multisort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_pad.md b/docs/internals/builtins/array/array_pad.md index bceca6273a..abf19d85a4 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -2,7 +2,7 @@ title: "array_pad() — internals" description: "Compiler internals for array_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 26 + order: 27 --- ## `array_pad()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/array/array_pop.md b/docs/internals/builtins/array/array_pop.md index aa4675a690..548befc220 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -2,7 +2,7 @@ title: "array_pop() — internals" description: "Compiler internals for array_pop(): lowering path, type checks, and runtime helpers." sidebar: - order: 27 + order: 28 --- ## `array_pop()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_pop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_product.md b/docs/internals/builtins/array/array_product.md index 38964fdfc1..4765c69ee2 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -2,7 +2,7 @@ title: "array_product() — internals" description: "Compiler internals for array_product(): lowering path, type checks, and runtime helpers." sidebar: - order: 28 + order: 29 --- ## `array_product()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_product.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_push.md b/docs/internals/builtins/array/array_push.md index 089ce72896..ef30414013 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -2,7 +2,7 @@ title: "array_push() — internals" description: "Compiler internals for array_push(): lowering path, type checks, and runtime helpers." sidebar: - order: 29 + order: 30 --- ## `array_push()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_push.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_rand.md b/docs/internals/builtins/array/array_rand.md index 3243ac7c42..a93a9dcb47 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -2,7 +2,7 @@ title: "array_rand() — internals" description: "Compiler internals for array_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 30 + order: 31 --- ## `array_rand()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_reduce.md b/docs/internals/builtins/array/array_reduce.md index 5b20e99844..0999473c32 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -2,7 +2,7 @@ title: "array_reduce() — internals" description: "Compiler internals for array_reduce(): lowering path, type checks, and runtime helpers." sidebar: - order: 31 + order: 32 --- ## `array_reduce()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reduce.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_replace.md b/docs/internals/builtins/array/array_replace.md index 719ba421e9..ab1454b0b8 100644 --- a/docs/internals/builtins/array/array_replace.md +++ b/docs/internals/builtins/array/array_replace.md @@ -2,7 +2,7 @@ title: "array_replace() — internals" description: "Compiler internals for array_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 32 + order: 33 --- ## `array_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_replace_recursive.md b/docs/internals/builtins/array/array_replace_recursive.md index d8cc6ae3b2..0d0c3c4443 100644 --- a/docs/internals/builtins/array/array_replace_recursive.md +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -2,7 +2,7 @@ title: "array_replace_recursive() — internals" description: "Compiler internals for array_replace_recursive(): lowering path, type checks, and runtime helpers." sidebar: - order: 33 + order: 34 --- ## `array_replace_recursive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_replace_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_replace_recursive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_reverse.md b/docs/internals/builtins/array/array_reverse.md index c81e1eda86..b6ded70fe3 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -2,7 +2,7 @@ title: "array_reverse() — internals" description: "Compiler internals for array_reverse(): lowering path, type checks, and runtime helpers." sidebar: - order: 34 + order: 35 --- ## `array_reverse()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_reverse.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function array_reverse(array $array): array +function array_reverse(array $array, bool $preserve_keys = false): array ``` ## What the type checker enforces -- **Arity**: takes exactly 1 argument. +- **Arity**: takes 1–2 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/array/array_search.md b/docs/internals/builtins/array/array_search.md index 912edb7fcd..a2618064ee 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -2,7 +2,7 @@ title: "array_search() — internals" description: "Compiler internals for array_search(): lowering path, type checks, and runtime helpers." sidebar: - order: 35 + order: 36 --- ## `array_search()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_search.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_shift.md b/docs/internals/builtins/array/array_shift.md index 339a8cd5f8..2c27dec658 100644 --- a/docs/internals/builtins/array/array_shift.md +++ b/docs/internals/builtins/array/array_shift.md @@ -2,7 +2,7 @@ title: "array_shift() — internals" description: "Compiler internals for array_shift(): lowering path, type checks, and runtime helpers." sidebar: - order: 36 + order: 37 --- ## `array_shift()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_shift.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_slice.md b/docs/internals/builtins/array/array_slice.md index 31d763c7e8..0b84ea7109 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -2,7 +2,7 @@ title: "array_slice() — internals" description: "Compiler internals for array_slice(): lowering path, type checks, and runtime helpers." sidebar: - order: 37 + order: 38 --- ## `array_slice()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_slice.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -24,7 +24,7 @@ sidebar: - **Target strategy**: `runtime_call` - **Validation**: `checker_hook` -- **Result type source**: `shared` +- **Result type source**: `checked` - **Result ownership**: `fresh` - **Effects**: `static (0 declared effects)` - **Requirements**: `static (0 requirements)` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function array_slice(array $array, int $offset, int $length = null): array +function array_slice(array $array, int $offset, int $length = null, bool $preserve_keys = false): array ``` ## What the type checker enforces -- **Arity**: takes 2–3 arguments (1 optional). +- **Arity**: takes 2–4 arguments (2 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/array/array_splice.md b/docs/internals/builtins/array/array_splice.md index 1c7d24dd47..ef0c4b6bf3 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -2,7 +2,7 @@ title: "array_splice() — internals" description: "Compiler internals for array_splice(): lowering path, type checks, and runtime helpers." sidebar: - order: 38 + order: 39 --- ## `array_splice()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_splice.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -25,7 +25,7 @@ sidebar: - **Target strategy**: `runtime_call` - **Validation**: `checker_hook` - **Result type source**: `checked` -- **Result ownership**: `may_alias_arguments` +- **Result ownership**: `fresh` - **Effects**: `static (16 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function array_splice(array $array, int $offset, int $length = null): array +function array_splice(array $array, int $offset, int $length = null, array $replacement = []): array ``` ## What the type checker enforces -- **Arity**: takes 2–3 arguments (1 optional). +- **Arity**: takes 2–4 arguments (2 optional). - **By-reference parameters**: `$array`. ## Eval interpreter (magician) diff --git a/docs/internals/builtins/array/array_sum.md b/docs/internals/builtins/array/array_sum.md index 00040b5b22..35de97dc19 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -2,7 +2,7 @@ title: "array_sum() — internals" description: "Compiler internals for array_sum(): lowering path, type checks, and runtime helpers." sidebar: - order: 39 + order: 40 --- ## `array_sum()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_sum.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_udiff.md b/docs/internals/builtins/array/array_udiff.md index 986beb6442..b3ce845ab5 100644 --- a/docs/internals/builtins/array/array_udiff.md +++ b/docs/internals/builtins/array/array_udiff.md @@ -2,7 +2,7 @@ title: "array_udiff() — internals" description: "Compiler internals for array_udiff(): lowering path, type checks, and runtime helpers." sidebar: - order: 40 + order: 41 --- ## `array_udiff()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_udiff.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_udiff.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_uintersect.md b/docs/internals/builtins/array/array_uintersect.md index 49dc06e4c1..ae6d7bc48f 100644 --- a/docs/internals/builtins/array/array_uintersect.md +++ b/docs/internals/builtins/array/array_uintersect.md @@ -2,7 +2,7 @@ title: "array_uintersect() — internals" description: "Compiler internals for array_uintersect(): lowering path, type checks, and runtime helpers." sidebar: - order: 41 + order: 42 --- ## `array_uintersect()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_uintersect.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_uintersect.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_unique.md b/docs/internals/builtins/array/array_unique.md index f657598ea5..64c8bb2d4d 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -2,7 +2,7 @@ title: "array_unique() — internals" description: "Compiler internals for array_unique(): lowering path, type checks, and runtime helpers." sidebar: - order: 42 + order: 43 --- ## `array_unique()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unique.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_unshift.md b/docs/internals/builtins/array/array_unshift.md index cc93e713ed..85236bee49 100644 --- a/docs/internals/builtins/array/array_unshift.md +++ b/docs/internals/builtins/array/array_unshift.md @@ -2,7 +2,7 @@ title: "array_unshift() — internals" description: "Compiler internals for array_unshift(): lowering path, type checks, and runtime helpers." sidebar: - order: 43 + order: 44 --- ## `array_unshift()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_unshift.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_values.md b/docs/internals/builtins/array/array_values.md index 1818524840..f6fea56aef 100644 --- a/docs/internals/builtins/array/array_values.md +++ b/docs/internals/builtins/array/array_values.md @@ -2,7 +2,7 @@ title: "array_values() — internals" description: "Compiler internals for array_values(): lowering path, type checks, and runtime helpers." sidebar: - order: 44 + order: 45 --- ## `array_values()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_values.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_walk.md b/docs/internals/builtins/array/array_walk.md index e898537c44..0252f07ddb 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -2,7 +2,7 @@ title: "array_walk() — internals" description: "Compiler internals for array_walk(): lowering path, type checks, and runtime helpers." sidebar: - order: 45 + order: 46 --- ## `array_walk()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/array_walk_recursive.md b/docs/internals/builtins/array/array_walk_recursive.md index 049c96f994..f6d4947f64 100644 --- a/docs/internals/builtins/array/array_walk_recursive.md +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -2,7 +2,7 @@ title: "array_walk_recursive() — internals" description: "Compiler internals for array_walk_recursive(): lowering path, type checks, and runtime helpers." sidebar: - order: 46 + order: 47 --- ## `array_walk_recursive()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/array_walk_recursive.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/array_walk_recursive.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/arsort.md b/docs/internals/builtins/array/arsort.md index 3be1f0a205..e88e64aeb3 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -2,7 +2,7 @@ title: "arsort() — internals" description: "Compiler internals for arsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 47 + order: 48 --- ## `arsort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/arsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/asort.md b/docs/internals/builtins/array/asort.md index 2f92f3391a..32cbf8a556 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -2,7 +2,7 @@ title: "asort() — internals" description: "Compiler internals for asort(): lowering path, type checks, and runtime helpers." sidebar: - order: 48 + order: 49 --- ## `asort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/asort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/call_user_func.md b/docs/internals/builtins/array/call_user_func.md index af4260cf8c..3f52f8bc50 100644 --- a/docs/internals/builtins/array/call_user_func.md +++ b/docs/internals/builtins/array/call_user_func.md @@ -2,7 +2,7 @@ title: "call_user_func() — internals" description: "Compiler internals for call_user_func(): lowering path, type checks, and runtime helpers." sidebar: - order: 49 + order: 50 --- ## `call_user_func()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/call_user_func.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/call_user_func_array.md b/docs/internals/builtins/array/call_user_func_array.md index 17c2a1ba2c..d978e752f7 100644 --- a/docs/internals/builtins/array/call_user_func_array.md +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -2,7 +2,7 @@ title: "call_user_func_array() — internals" description: "Compiler internals for call_user_func_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 50 + order: 51 --- ## `call_user_func_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/call_user_func_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index a74ae56c12..04769e3d48 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -2,7 +2,7 @@ title: "count() — internals" description: "Compiler internals for count(): lowering path, type checks, and runtime helpers." sidebar: - order: 51 + order: 52 --- ## `count()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/current.md b/docs/internals/builtins/array/current.md new file mode 100644 index 0000000000..e5a0beebeb --- /dev/null +++ b/docs/internals/builtins/array/current.md @@ -0,0 +1,56 @@ +--- +title: "current() — internals" +description: "Compiler internals for current(): lowering path, type checks, and runtime helpers." +sidebar: + order: 53 +--- + +## `current()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/current.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/current.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_value` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_value` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function current(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/current.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/current.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `current()`](../../../php/builtins/array/current.md) diff --git a/docs/internals/builtins/array/end.md b/docs/internals/builtins/array/end.md new file mode 100644 index 0000000000..038174b432 --- /dev/null +++ b/docs/internals/builtins/array/end.md @@ -0,0 +1,58 @@ +--- +title: "end() — internals" +description: "Compiler internals for end(): lowering path, type checks, and runtime helpers." +sidebar: + order: 54 +--- + +## `end()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/end.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/end.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_seek` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function end(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **By-reference parameters**: `$array`. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/end.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/end.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + +## Cross-references + +- [User reference for `end()`](../../../php/builtins/array/end.md) diff --git a/docs/internals/builtins/array/in_array.md b/docs/internals/builtins/array/in_array.md index 139dc653e4..e2ca0c6b3f 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -2,7 +2,7 @@ title: "in_array() — internals" description: "Compiler internals for in_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 52 + order: 55 --- ## `in_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/in_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/key.md b/docs/internals/builtins/array/key.md new file mode 100644 index 0000000000..52d168cd2a --- /dev/null +++ b/docs/internals/builtins/array/key.md @@ -0,0 +1,56 @@ +--- +title: "key() — internals" +description: "Compiler internals for key(): lowering path, type checks, and runtime helpers." +sidebar: + order: 56 +--- + +## `key()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/key.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/key.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_key` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_key` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function key(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/key.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `key()`](../../../php/builtins/array/key.md) diff --git a/docs/internals/builtins/array/krsort.md b/docs/internals/builtins/array/krsort.md index ee91eefd4d..beb74cf063 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -2,7 +2,7 @@ title: "krsort() — internals" description: "Compiler internals for krsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 53 + order: 57 --- ## `krsort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/krsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/ksort.md b/docs/internals/builtins/array/ksort.md index 900d93ee35..07f906b2f4 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -2,7 +2,7 @@ title: "ksort() — internals" description: "Compiler internals for ksort(): lowering path, type checks, and runtime helpers." sidebar: - order: 54 + order: 58 --- ## `ksort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/ksort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/natcasesort.md b/docs/internals/builtins/array/natcasesort.md index aaffe78fe1..8374a86e45 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -2,7 +2,7 @@ title: "natcasesort() — internals" description: "Compiler internals for natcasesort(): lowering path, type checks, and runtime helpers." sidebar: - order: 55 + order: 59 --- ## `natcasesort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natcasesort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/natsort.md b/docs/internals/builtins/array/natsort.md index 100ad05cc3..ac78826685 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -2,7 +2,7 @@ title: "natsort() — internals" description: "Compiler internals for natsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 56 + order: 60 --- ## `natsort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/natsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/next.md b/docs/internals/builtins/array/next.md new file mode 100644 index 0000000000..f9afebdbb4 --- /dev/null +++ b/docs/internals/builtins/array/next.md @@ -0,0 +1,58 @@ +--- +title: "next() — internals" +description: "Compiler internals for next(): lowering path, type checks, and runtime helpers." +sidebar: + order: 61 +--- + +## `next()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/next.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/next.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_seek` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function next(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **By-reference parameters**: `$array`. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/next.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/next.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + +## Cross-references + +- [User reference for `next()`](../../../php/builtins/array/next.md) diff --git a/docs/internals/builtins/array/prev.md b/docs/internals/builtins/array/prev.md new file mode 100644 index 0000000000..9c9fe72ffa --- /dev/null +++ b/docs/internals/builtins/array/prev.md @@ -0,0 +1,58 @@ +--- +title: "prev() — internals" +description: "Compiler internals for prev(): lowering path, type checks, and runtime helpers." +sidebar: + order: 62 +--- + +## `prev()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/prev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/prev.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_seek` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function prev(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **By-reference parameters**: `$array`. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/prev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/prev.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + +## Cross-references + +- [User reference for `prev()`](../../../php/builtins/array/prev.md) diff --git a/docs/internals/builtins/array/range.md b/docs/internals/builtins/array/range.md index e25adb2a41..dff11b03b0 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -2,7 +2,7 @@ title: "range() — internals" description: "Compiler internals for range(): lowering path, type checks, and runtime helpers." sidebar: - order: 57 + order: 63 --- ## `range()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/range.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function range(mixed $start, mixed $end): array +function range(mixed $start, mixed $end, int $step = 1): array ``` ## What the type checker enforces -- **Arity**: takes exactly 2 arguments. +- **Arity**: takes 2–3 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/array/reset.md b/docs/internals/builtins/array/reset.md new file mode 100644 index 0000000000..bff8359f26 --- /dev/null +++ b/docs/internals/builtins/array/reset.md @@ -0,0 +1,58 @@ +--- +title: "reset() — internals" +description: "Compiler internals for reset(): lowering path, type checks, and runtime helpers." +sidebar: + order: 64 +--- + +## `reset()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/array/reset.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/reset.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (16 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.array_ptr_seek` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function reset(array $array): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. +- **By-reference parameters**: `$array`. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/reset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/reset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + +## Cross-references + +- [User reference for `reset()`](../../../php/builtins/array/reset.md) diff --git a/docs/internals/builtins/array/rsort.md b/docs/internals/builtins/array/rsort.md index 4760fda420..091123d0b0 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -2,7 +2,7 @@ title: "rsort() — internals" description: "Compiler internals for rsort(): lowering path, type checks, and runtime helpers." sidebar: - order: 58 + order: 65 --- ## `rsort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/rsort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/shuffle.md b/docs/internals/builtins/array/shuffle.md index 6411b31e4b..6b58be8cfb 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -2,7 +2,7 @@ title: "shuffle() — internals" description: "Compiler internals for shuffle(): lowering path, type checks, and runtime helpers." sidebar: - order: 59 + order: 66 --- ## `shuffle()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/shuffle.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/sort.md b/docs/internals/builtins/array/sort.md index c3bc18387e..63835a05f1 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -2,7 +2,7 @@ title: "sort() — internals" description: "Compiler internals for sort(): lowering path, type checks, and runtime helpers." sidebar: - order: 60 + order: 67 --- ## `sort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/sort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/uasort.md b/docs/internals/builtins/array/uasort.md index 1c004be9b9..82b2916643 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -2,7 +2,7 @@ title: "uasort() — internals" description: "Compiler internals for uasort(): lowering path, type checks, and runtime helpers." sidebar: - order: 61 + order: 68 --- ## `uasort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uasort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/uksort.md b/docs/internals/builtins/array/uksort.md index e5be4cfd65..31f20913ce 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -2,7 +2,7 @@ title: "uksort() — internals" description: "Compiler internals for uksort(): lowering path, type checks, and runtime helpers." sidebar: - order: 62 + order: 69 --- ## `uksort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/uksort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/array/usort.md b/docs/internals/builtins/array/usort.md index e1f10a62f1..eef6e9f750 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -2,7 +2,7 @@ title: "usort() — internals" description: "Compiler internals for usort(): lowering path, type checks, and runtime helpers." sidebar: - order: 63 + order: 70 --- ## `usort()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/usort.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/buffer/buffer_free.md b/docs/internals/builtins/buffer/buffer_free.md index c77febbc26..243015674a 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -2,7 +2,7 @@ title: "buffer_free() — internals" description: "Compiler internals for buffer_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 64 + order: 71 --- ## `buffer_free()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_free.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/buffer/buffer_len.md b/docs/internals/builtins/buffer/buffer_len.md index e1a6d53786..ec481410c8 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -2,7 +2,7 @@ title: "buffer_len() — internals" description: "Compiler internals for buffer_len(): lowering path, type checks, and runtime helpers." sidebar: - order: 65 + order: 72 --- ## `buffer_len()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/buffer_len.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_alias.md b/docs/internals/builtins/class/class_alias.md index 184a49cd5e..c64a5e2a01 100644 --- a/docs/internals/builtins/class/class_alias.md +++ b/docs/internals/builtins/class/class_alias.md @@ -2,7 +2,7 @@ title: "class_alias() — internals" description: "Compiler internals for class_alias(): lowering path, type checks, and runtime helpers." sidebar: - order: 66 + order: 73 --- ## `class_alias()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_alias.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_attribute_args.md b/docs/internals/builtins/class/class_attribute_args.md index b11286082e..3743882206 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -2,7 +2,7 @@ title: "class_attribute_args() — internals" description: "Compiler internals for class_attribute_args(): lowering path, type checks, and runtime helpers." sidebar: - order: 67 + order: 74 --- ## `class_attribute_args()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_args.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_attribute_names.md b/docs/internals/builtins/class/class_attribute_names.md index 992947a798..82bf1a2b6e 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -2,7 +2,7 @@ title: "class_attribute_names() — internals" description: "Compiler internals for class_attribute_names(): lowering path, type checks, and runtime helpers." sidebar: - order: 68 + order: 75 --- ## `class_attribute_names()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_names.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index 5d9a604609..a02058b698 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -2,7 +2,7 @@ title: "class_exists() — internals" description: "Compiler internals for class_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 69 + order: 76 --- ## `class_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_get_attributes.md b/docs/internals/builtins/class/class_get_attributes.md index c51d7c0204..c52559970f 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -2,7 +2,7 @@ title: "class_get_attributes() — internals" description: "Compiler internals for class_get_attributes(): lowering path, type checks, and runtime helpers." sidebar: - order: 70 + order: 77 --- ## `class_get_attributes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_get_attributes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_implements.md b/docs/internals/builtins/class/class_implements.md index 33c011fe59..743d46cb13 100644 --- a/docs/internals/builtins/class/class_implements.md +++ b/docs/internals/builtins/class/class_implements.md @@ -2,7 +2,7 @@ title: "class_implements() — internals" description: "Compiler internals for class_implements(): lowering path, type checks, and runtime helpers." sidebar: - order: 71 + order: 78 --- ## `class_implements()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_implements.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_parents.md b/docs/internals/builtins/class/class_parents.md index 002e6ccaa1..467b91b262 100644 --- a/docs/internals/builtins/class/class_parents.md +++ b/docs/internals/builtins/class/class_parents.md @@ -2,7 +2,7 @@ title: "class_parents() — internals" description: "Compiler internals for class_parents(): lowering path, type checks, and runtime helpers." sidebar: - order: 72 + order: 79 --- ## `class_parents()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_parents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/class_uses.md b/docs/internals/builtins/class/class_uses.md index 68e47eabaf..bb53b77cf3 100644 --- a/docs/internals/builtins/class/class_uses.md +++ b/docs/internals/builtins/class/class_uses.md @@ -2,7 +2,7 @@ title: "class_uses() — internals" description: "Compiler internals for class_uses(): lowering path, type checks, and runtime helpers." sidebar: - order: 73 + order: 80 --- ## `class_uses()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_uses.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 48f23d6fb6..b7f7ebcb62 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -2,7 +2,7 @@ title: "enum_exists() — internals" description: "Compiler internals for enum_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 74 + order: 81 --- ## `enum_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/enum_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index fb2714c755..c43ddb72d8 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -2,7 +2,7 @@ title: "function_exists() — internals" description: "Compiler internals for function_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 75 + order: 82 --- ## `function_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_called_class.md b/docs/internals/builtins/class/get_called_class.md index 193a5d2c75..2144d5044f 100644 --- a/docs/internals/builtins/class/get_called_class.md +++ b/docs/internals/builtins/class/get_called_class.md @@ -2,7 +2,7 @@ title: "get_called_class() — internals" description: "Compiler internals for get_called_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 76 + order: 83 --- ## `get_called_class()` — internals diff --git a/docs/internals/builtins/class/get_class.md b/docs/internals/builtins/class/get_class.md index a503f39ca1..fc043ac932 100644 --- a/docs/internals/builtins/class/get_class.md +++ b/docs/internals/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class() — internals" description: "Compiler internals for get_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 77 + order: 84 --- ## `get_class()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_class.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_class_methods.md b/docs/internals/builtins/class/get_class_methods.md index b1c166727b..8ba8eb4f36 100644 --- a/docs/internals/builtins/class/get_class_methods.md +++ b/docs/internals/builtins/class/get_class_methods.md @@ -2,7 +2,7 @@ title: "get_class_methods() — internals" description: "Compiler internals for get_class_methods(): lowering path, type checks, and runtime helpers." sidebar: - order: 78 + order: 85 --- ## `get_class_methods()` — internals diff --git a/docs/internals/builtins/class/get_class_vars.md b/docs/internals/builtins/class/get_class_vars.md index b2ecbf56ed..8c09626d52 100644 --- a/docs/internals/builtins/class/get_class_vars.md +++ b/docs/internals/builtins/class/get_class_vars.md @@ -2,7 +2,7 @@ title: "get_class_vars() — internals" description: "Compiler internals for get_class_vars(): lowering path, type checks, and runtime helpers." sidebar: - order: 79 + order: 86 --- ## `get_class_vars()` — internals diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index 7a37dcc7dd..6988db632d 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes() — internals" description: "Compiler internals for get_declared_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 80 + order: 87 --- ## `get_declared_classes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_classes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index 44f754cedf..9fbe4addd9 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces() — internals" description: "Compiler internals for get_declared_interfaces(): lowering path, type checks, and runtime helpers." sidebar: - order: 81 + order: 88 --- ## `get_declared_interfaces()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_interfaces.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index 9ca679cd97..4b63544e05 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits() — internals" description: "Compiler internals for get_declared_traits(): lowering path, type checks, and runtime helpers." sidebar: - order: 82 + order: 89 --- ## `get_declared_traits()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_traits.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/get_object_vars.md b/docs/internals/builtins/class/get_object_vars.md index d6fd181ec5..dd533e1f69 100644 --- a/docs/internals/builtins/class/get_object_vars.md +++ b/docs/internals/builtins/class/get_object_vars.md @@ -2,7 +2,7 @@ title: "get_object_vars() — internals" description: "Compiler internals for get_object_vars(): lowering path, type checks, and runtime helpers." sidebar: - order: 83 + order: 90 --- ## `get_object_vars()` — internals diff --git a/docs/internals/builtins/class/get_parent_class.md b/docs/internals/builtins/class/get_parent_class.md index ddcc193316..e96c2a77ae 100644 --- a/docs/internals/builtins/class/get_parent_class.md +++ b/docs/internals/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class() — internals" description: "Compiler internals for get_parent_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 84 + order: 91 --- ## `get_parent_class()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_parent_class.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index d538d1f6f6..cc3693b6f9 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists() — internals" description: "Compiler internals for interface_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 85 + order: 92 --- ## `interface_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/interface_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/is_a.md b/docs/internals/builtins/class/is_a.md index a997d94244..bbc7618010 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a() — internals" description: "Compiler internals for is_a(): lowering path, type checks, and runtime helpers." sidebar: - order: 86 + order: 93 --- ## `is_a()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_a.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/is_subclass_of.md b/docs/internals/builtins/class/is_subclass_of.md index 362d74c829..99fcbe41d7 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of() — internals" description: "Compiler internals for is_subclass_of(): lowering path, type checks, and runtime helpers." sidebar: - order: 87 + order: 94 --- ## `is_subclass_of()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/is_subclass_of.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/method_exists.md b/docs/internals/builtins/class/method_exists.md index d2a2ec0242..d755698dd3 100644 --- a/docs/internals/builtins/class/method_exists.md +++ b/docs/internals/builtins/class/method_exists.md @@ -2,7 +2,7 @@ title: "method_exists() — internals" description: "Compiler internals for method_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 88 + order: 95 --- ## `method_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/method_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/method_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/property_exists.md b/docs/internals/builtins/class/property_exists.md index 09498cab73..8d39cae093 100644 --- a/docs/internals/builtins/class/property_exists.md +++ b/docs/internals/builtins/class/property_exists.md @@ -2,7 +2,7 @@ title: "property_exists() — internals" description: "Compiler internals for property_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 89 + order: 96 --- ## `property_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/property_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/property_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index adecf06c3f..d7d8f05a87 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists() — internals" description: "Compiler internals for trait_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 90 + order: 97 --- ## `trait_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/trait_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/checkdate.md b/docs/internals/builtins/date/checkdate.md index 25de9fac83..8573b719f8 100644 --- a/docs/internals/builtins/date/checkdate.md +++ b/docs/internals/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate() — internals" description: "Compiler internals for checkdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 91 + order: 98 --- ## `checkdate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/checkdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/date.md b/docs/internals/builtins/date/date.md index e681f1cb9e..2687993388 100644 --- a/docs/internals/builtins/date/date.md +++ b/docs/internals/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date() — internals" description: "Compiler internals for date(): lowering path, type checks, and runtime helpers." sidebar: - order: 92 + order: 99 --- ## `date()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/date.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/date_default_timezone_get.md b/docs/internals/builtins/date/date_default_timezone_get.md index 0c66ae0117..ab5731f768 100644 --- a/docs/internals/builtins/date/date_default_timezone_get.md +++ b/docs/internals/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get() — internals" description: "Compiler internals for date_default_timezone_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 93 + order: 100 --- ## `date_default_timezone_get()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date_default_timezone_get.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/date_default_timezone_set.md b/docs/internals/builtins/date/date_default_timezone_set.md index 18748dae59..dda01b766c 100644 --- a/docs/internals/builtins/date/date_default_timezone_set.md +++ b/docs/internals/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set() — internals" description: "Compiler internals for date_default_timezone_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 94 + order: 101 --- ## `date_default_timezone_set()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/date_default_timezone_set.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/getdate.md b/docs/internals/builtins/date/getdate.md index 4e6dfb8be7..18c30322ec 100644 --- a/docs/internals/builtins/date/getdate.md +++ b/docs/internals/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate() — internals" description: "Compiler internals for getdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 95 + order: 102 --- ## `getdate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/gmdate.md b/docs/internals/builtins/date/gmdate.md index 8b5e09fe0d..4f48f82ca0 100644 --- a/docs/internals/builtins/date/gmdate.md +++ b/docs/internals/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate() — internals" description: "Compiler internals for gmdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 96 + order: 103 --- ## `gmdate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmdate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/gmmktime.md b/docs/internals/builtins/date/gmmktime.md index b1ea4bc127..6d84c3ce72 100644 --- a/docs/internals/builtins/date/gmmktime.md +++ b/docs/internals/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime() — internals" description: "Compiler internals for gmmktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 97 + order: 104 --- ## `gmmktime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/gmmktime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/hrtime.md b/docs/internals/builtins/date/hrtime.md index d315354428..6f811df9bd 100644 --- a/docs/internals/builtins/date/hrtime.md +++ b/docs/internals/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime() — internals" description: "Compiler internals for hrtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 98 + order: 105 --- ## `hrtime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/hrtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/localtime.md b/docs/internals/builtins/date/localtime.md index 243770addb..71d4b804af 100644 --- a/docs/internals/builtins/date/localtime.md +++ b/docs/internals/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime() — internals" description: "Compiler internals for localtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 99 + order: 106 --- ## `localtime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/localtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/microtime.md b/docs/internals/builtins/date/microtime.md index 3deb258af9..ca15e14585 100644 --- a/docs/internals/builtins/date/microtime.md +++ b/docs/internals/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime() — internals" description: "Compiler internals for microtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 100 + order: 107 --- ## `microtime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/microtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/mktime.md b/docs/internals/builtins/date/mktime.md index 78186a06a3..ba2e1f7163 100644 --- a/docs/internals/builtins/date/mktime.md +++ b/docs/internals/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime() — internals" description: "Compiler internals for mktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 101 + order: 108 --- ## `mktime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/mktime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/strtotime.md b/docs/internals/builtins/date/strtotime.md index c6bdde6a8b..8be8ff81d1 100644 --- a/docs/internals/builtins/date/strtotime.md +++ b/docs/internals/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime() — internals" description: "Compiler internals for strtotime(): lowering path, type checks, and runtime helpers." sidebar: - order: 102 + order: 109 --- ## `strtotime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/strtotime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/date/time.md b/docs/internals/builtins/date/time.md index e9d1f1b594..2f4f512e08 100644 --- a/docs/internals/builtins/date/time.md +++ b/docs/internals/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time() — internals" description: "Compiler internals for time(): lowering path, type checks, and runtime helpers." sidebar: - order: 103 + order: 110 --- ## `time()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/time.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/time.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/basename.md b/docs/internals/builtins/filesystem/basename.md index 3a742bf386..224ebef1e2 100644 --- a/docs/internals/builtins/filesystem/basename.md +++ b/docs/internals/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename() — internals" description: "Compiler internals for basename(): lowering path, type checks, and runtime helpers." sidebar: - order: 104 + order: 111 --- ## `basename()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/basename.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chdir.md b/docs/internals/builtins/filesystem/chdir.md index efbb0bae9c..56112dab3d 100644 --- a/docs/internals/builtins/filesystem/chdir.md +++ b/docs/internals/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir() — internals" description: "Compiler internals for chdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 105 + order: 112 --- ## `chdir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chgrp.md b/docs/internals/builtins/filesystem/chgrp.md index 1f3f31cb8d..d5363988f1 100644 --- a/docs/internals/builtins/filesystem/chgrp.md +++ b/docs/internals/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp() — internals" description: "Compiler internals for chgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 106 + order: 113 --- ## `chgrp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chgrp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chmod.md b/docs/internals/builtins/filesystem/chmod.md index c9a47c4b09..583d3cf315 100644 --- a/docs/internals/builtins/filesystem/chmod.md +++ b/docs/internals/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod() — internals" description: "Compiler internals for chmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 107 + order: 114 --- ## `chmod()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chmod.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/chown.md b/docs/internals/builtins/filesystem/chown.md index 6177ea3c58..0c3908ea02 100644 --- a/docs/internals/builtins/filesystem/chown.md +++ b/docs/internals/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown() — internals" description: "Compiler internals for chown(): lowering path, type checks, and runtime helpers." sidebar: - order: 108 + order: 115 --- ## `chown()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/chown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/clearstatcache.md b/docs/internals/builtins/filesystem/clearstatcache.md index 2e4ce72c7f..e7adae6e59 100644 --- a/docs/internals/builtins/filesystem/clearstatcache.md +++ b/docs/internals/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache() — internals" description: "Compiler internals for clearstatcache(): lowering path, type checks, and runtime helpers." sidebar: - order: 109 + order: 116 --- ## `clearstatcache()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/clearstatcache.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/copy.md b/docs/internals/builtins/filesystem/copy.md index 6fcff4a26b..50c77c3fe9 100644 --- a/docs/internals/builtins/filesystem/copy.md +++ b/docs/internals/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy() — internals" description: "Compiler internals for copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 110 + order: 117 --- ## `copy()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/copy.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/dirname.md b/docs/internals/builtins/filesystem/dirname.md index d725635d8c..a3fdcd75a1 100644 --- a/docs/internals/builtins/filesystem/dirname.md +++ b/docs/internals/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname() — internals" description: "Compiler internals for dirname(): lowering path, type checks, and runtime helpers." sidebar: - order: 111 + order: 118 --- ## `dirname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/dirname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/disk_free_space.md b/docs/internals/builtins/filesystem/disk_free_space.md index 9e77c267a8..f2e01ef594 100644 --- a/docs/internals/builtins/filesystem/disk_free_space.md +++ b/docs/internals/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space() — internals" description: "Compiler internals for disk_free_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 112 + order: 119 --- ## `disk_free_space()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_free_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/disk_total_space.md b/docs/internals/builtins/filesystem/disk_total_space.md index ffed9dfcf7..fcb263fc00 100644 --- a/docs/internals/builtins/filesystem/disk_total_space.md +++ b/docs/internals/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space() — internals" description: "Compiler internals for disk_total_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 113 + order: 120 --- ## `disk_total_space()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/disk_total_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/file_exists.md b/docs/internals/builtins/filesystem/file_exists.md index d090945e1f..35337861b4 100644 --- a/docs/internals/builtins/filesystem/file_exists.md +++ b/docs/internals/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists() — internals" description: "Compiler internals for file_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 114 + order: 121 --- ## `file_exists()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_exists.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileatime.md b/docs/internals/builtins/filesystem/fileatime.md index eab7694a78..6058c2c52f 100644 --- a/docs/internals/builtins/filesystem/fileatime.md +++ b/docs/internals/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime() — internals" description: "Compiler internals for fileatime(): lowering path, type checks, and runtime helpers." sidebar: - order: 115 + order: 122 --- ## `fileatime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileatime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filectime.md b/docs/internals/builtins/filesystem/filectime.md index 1060c028f5..9d91da056d 100644 --- a/docs/internals/builtins/filesystem/filectime.md +++ b/docs/internals/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime() — internals" description: "Compiler internals for filectime(): lowering path, type checks, and runtime helpers." sidebar: - order: 116 + order: 123 --- ## `filectime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filectime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filegroup.md b/docs/internals/builtins/filesystem/filegroup.md index 1ebde20221..2e169489a2 100644 --- a/docs/internals/builtins/filesystem/filegroup.md +++ b/docs/internals/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup() — internals" description: "Compiler internals for filegroup(): lowering path, type checks, and runtime helpers." sidebar: - order: 117 + order: 124 --- ## `filegroup()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filegroup.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileinode.md b/docs/internals/builtins/filesystem/fileinode.md index d194b9abf6..a998f3aaf8 100644 --- a/docs/internals/builtins/filesystem/fileinode.md +++ b/docs/internals/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode() — internals" description: "Compiler internals for fileinode(): lowering path, type checks, and runtime helpers." sidebar: - order: 118 + order: 125 --- ## `fileinode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileinode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filemtime.md b/docs/internals/builtins/filesystem/filemtime.md index 14714b3a80..020e636aff 100644 --- a/docs/internals/builtins/filesystem/filemtime.md +++ b/docs/internals/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime() — internals" description: "Compiler internals for filemtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 119 + order: 126 --- ## `filemtime()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filemtime.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileowner.md b/docs/internals/builtins/filesystem/fileowner.md index 28944a353e..bc99c2dbb1 100644 --- a/docs/internals/builtins/filesystem/fileowner.md +++ b/docs/internals/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner() — internals" description: "Compiler internals for fileowner(): lowering path, type checks, and runtime helpers." sidebar: - order: 120 + order: 127 --- ## `fileowner()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileowner.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fileperms.md b/docs/internals/builtins/filesystem/fileperms.md index cdacc74efc..94b5a0da03 100644 --- a/docs/internals/builtins/filesystem/fileperms.md +++ b/docs/internals/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms() — internals" description: "Compiler internals for fileperms(): lowering path, type checks, and runtime helpers." sidebar: - order: 121 + order: 128 --- ## `fileperms()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fileperms.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filesize.md b/docs/internals/builtins/filesystem/filesize.md index 3f75489563..b7be40a529 100644 --- a/docs/internals/builtins/filesystem/filesize.md +++ b/docs/internals/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize() — internals" description: "Compiler internals for filesize(): lowering path, type checks, and runtime helpers." sidebar: - order: 122 + order: 129 --- ## `filesize()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filesize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/filetype.md b/docs/internals/builtins/filesystem/filetype.md index 4b35df56cc..642a7a5716 100644 --- a/docs/internals/builtins/filesystem/filetype.md +++ b/docs/internals/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype() — internals" description: "Compiler internals for filetype(): lowering path, type checks, and runtime helpers." sidebar: - order: 123 + order: 130 --- ## `filetype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/filetype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/fnmatch.md b/docs/internals/builtins/filesystem/fnmatch.md index 18e5d20381..6435743f62 100644 --- a/docs/internals/builtins/filesystem/fnmatch.md +++ b/docs/internals/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch() — internals" description: "Compiler internals for fnmatch(): lowering path, type checks, and runtime helpers." sidebar: - order: 124 + order: 131 --- ## `fnmatch()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fnmatch.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/getcwd.md b/docs/internals/builtins/filesystem/getcwd.md index bb1da2f0be..0e72a852a9 100644 --- a/docs/internals/builtins/filesystem/getcwd.md +++ b/docs/internals/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd() — internals" description: "Compiler internals for getcwd(): lowering path, type checks, and runtime helpers." sidebar: - order: 125 + order: 132 --- ## `getcwd()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getcwd.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/getenv.md b/docs/internals/builtins/filesystem/getenv.md index 3a0ec1fb95..1cc9cf9339 100644 --- a/docs/internals/builtins/filesystem/getenv.md +++ b/docs/internals/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv() — internals" description: "Compiler internals for getenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 126 + order: 133 --- ## `getenv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/getenv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/glob.md b/docs/internals/builtins/filesystem/glob.md index 1854afc8ca..0583378e38 100644 --- a/docs/internals/builtins/filesystem/glob.md +++ b/docs/internals/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob() — internals" description: "Compiler internals for glob(): lowering path, type checks, and runtime helpers." sidebar: - order: 127 + order: 134 --- ## `glob()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/glob.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_dir.md b/docs/internals/builtins/filesystem/is_dir.md index 0ceaea1dfe..6e4504eb40 100644 --- a/docs/internals/builtins/filesystem/is_dir.md +++ b/docs/internals/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir() — internals" description: "Compiler internals for is_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 128 + order: 135 --- ## `is_dir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_dir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_executable.md b/docs/internals/builtins/filesystem/is_executable.md index b5ba7f30f4..cf61bc7cb6 100644 --- a/docs/internals/builtins/filesystem/is_executable.md +++ b/docs/internals/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable() — internals" description: "Compiler internals for is_executable(): lowering path, type checks, and runtime helpers." sidebar: - order: 129 + order: 136 --- ## `is_executable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_executable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_file.md b/docs/internals/builtins/filesystem/is_file.md index 132b6fa83b..53234b0b07 100644 --- a/docs/internals/builtins/filesystem/is_file.md +++ b/docs/internals/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file() — internals" description: "Compiler internals for is_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 130 + order: 137 --- ## `is_file()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_link.md b/docs/internals/builtins/filesystem/is_link.md index 36392d0881..9faf662961 100644 --- a/docs/internals/builtins/filesystem/is_link.md +++ b/docs/internals/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link() — internals" description: "Compiler internals for is_link(): lowering path, type checks, and runtime helpers." sidebar: - order: 131 + order: 138 --- ## `is_link()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_link.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_readable.md b/docs/internals/builtins/filesystem/is_readable.md index 7148706cd8..a696da9de3 100644 --- a/docs/internals/builtins/filesystem/is_readable.md +++ b/docs/internals/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable() — internals" description: "Compiler internals for is_readable(): lowering path, type checks, and runtime helpers." sidebar: - order: 132 + order: 139 --- ## `is_readable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_readable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_writable.md b/docs/internals/builtins/filesystem/is_writable.md index c9aaeeec8f..a47b689b55 100644 --- a/docs/internals/builtins/filesystem/is_writable.md +++ b/docs/internals/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable() — internals" description: "Compiler internals for is_writable(): lowering path, type checks, and runtime helpers." sidebar: - order: 133 + order: 140 --- ## `is_writable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/is_writeable.md b/docs/internals/builtins/filesystem/is_writeable.md index 6325d09f4a..560fb43ec5 100644 --- a/docs/internals/builtins/filesystem/is_writeable.md +++ b/docs/internals/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable() — internals" description: "Compiler internals for is_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 134 + order: 141 --- ## `is_writeable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/is_writeable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/lchgrp.md b/docs/internals/builtins/filesystem/lchgrp.md index 98d9f077bd..7e43ca6b91 100644 --- a/docs/internals/builtins/filesystem/lchgrp.md +++ b/docs/internals/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp() — internals" description: "Compiler internals for lchgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 135 + order: 142 --- ## `lchgrp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchgrp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/lchown.md b/docs/internals/builtins/filesystem/lchown.md index 9d47e90e68..bf0d962eb6 100644 --- a/docs/internals/builtins/filesystem/lchown.md +++ b/docs/internals/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown() — internals" description: "Compiler internals for lchown(): lowering path, type checks, and runtime helpers." sidebar: - order: 136 + order: 143 --- ## `lchown()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lchown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/link.md b/docs/internals/builtins/filesystem/link.md index 42c0cbaa5d..dd8d11a97b 100644 --- a/docs/internals/builtins/filesystem/link.md +++ b/docs/internals/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link() — internals" description: "Compiler internals for link(): lowering path, type checks, and runtime helpers." sidebar: - order: 137 + order: 144 --- ## `link()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/link.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/link.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/linkinfo.md b/docs/internals/builtins/filesystem/linkinfo.md index e26510acc6..b988fedfc8 100644 --- a/docs/internals/builtins/filesystem/linkinfo.md +++ b/docs/internals/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo() — internals" description: "Compiler internals for linkinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 138 + order: 145 --- ## `linkinfo()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/linkinfo.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/lstat.md b/docs/internals/builtins/filesystem/lstat.md index 09a9a006b8..427fc6d848 100644 --- a/docs/internals/builtins/filesystem/lstat.md +++ b/docs/internals/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat() — internals" description: "Compiler internals for lstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 139 + order: 146 --- ## `lstat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/lstat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/mkdir.md b/docs/internals/builtins/filesystem/mkdir.md index 5829147904..c57af9b573 100644 --- a/docs/internals/builtins/filesystem/mkdir.md +++ b/docs/internals/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir() — internals" description: "Compiler internals for mkdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 140 + order: 147 --- ## `mkdir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/mkdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/pathinfo.md b/docs/internals/builtins/filesystem/pathinfo.md index 4992d15be7..13ab5e6d7a 100644 --- a/docs/internals/builtins/filesystem/pathinfo.md +++ b/docs/internals/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo() — internals" description: "Compiler internals for pathinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 141 + order: 148 --- ## `pathinfo()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pathinfo.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/putenv.md b/docs/internals/builtins/filesystem/putenv.md index 4711e72778..f847cf8261 100644 --- a/docs/internals/builtins/filesystem/putenv.md +++ b/docs/internals/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv() — internals" description: "Compiler internals for putenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 142 + order: 149 --- ## `putenv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/putenv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/readfile.md b/docs/internals/builtins/filesystem/readfile.md index 1dbecb9fe3..326041af70 100644 --- a/docs/internals/builtins/filesystem/readfile.md +++ b/docs/internals/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile() — internals" description: "Compiler internals for readfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 143 + order: 150 --- ## `readfile()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readfile.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/readlink.md b/docs/internals/builtins/filesystem/readlink.md index edc5ef8261..9b59b39fa0 100644 --- a/docs/internals/builtins/filesystem/readlink.md +++ b/docs/internals/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink() — internals" description: "Compiler internals for readlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 144 + order: 151 --- ## `readlink()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath.md b/docs/internals/builtins/filesystem/realpath.md index 777e4bda8c..b72442f5b7 100644 --- a/docs/internals/builtins/filesystem/realpath.md +++ b/docs/internals/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath() — internals" description: "Compiler internals for realpath(): lowering path, type checks, and runtime helpers." sidebar: - order: 145 + order: 152 --- ## `realpath()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath_cache_get.md b/docs/internals/builtins/filesystem/realpath_cache_get.md index 88bc38b6e5..6f2691f86c 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_get.md +++ b/docs/internals/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get() — internals" description: "Compiler internals for realpath_cache_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 146 + order: 153 --- ## `realpath_cache_get()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_get.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/realpath_cache_size.md b/docs/internals/builtins/filesystem/realpath_cache_size.md index 182f5990e1..b92506e4c3 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_size.md +++ b/docs/internals/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size() — internals" description: "Compiler internals for realpath_cache_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 147 + order: 154 --- ## `realpath_cache_size()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/realpath_cache_size.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/rename.md b/docs/internals/builtins/filesystem/rename.md index ec8d5980d1..dbfb5b4ae4 100644 --- a/docs/internals/builtins/filesystem/rename.md +++ b/docs/internals/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename() — internals" description: "Compiler internals for rename(): lowering path, type checks, and runtime helpers." sidebar: - order: 148 + order: 155 --- ## `rename()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rename.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/rmdir.md b/docs/internals/builtins/filesystem/rmdir.md index 50db35d237..724eb68995 100644 --- a/docs/internals/builtins/filesystem/rmdir.md +++ b/docs/internals/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir() — internals" description: "Compiler internals for rmdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 149 + order: 156 --- ## `rmdir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rmdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/scandir.md b/docs/internals/builtins/filesystem/scandir.md index 8a1213bbfb..aa947a0df2 100644 --- a/docs/internals/builtins/filesystem/scandir.md +++ b/docs/internals/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir() — internals" description: "Compiler internals for scandir(): lowering path, type checks, and runtime helpers." sidebar: - order: 150 + order: 157 --- ## `scandir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/scandir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/stat.md b/docs/internals/builtins/filesystem/stat.md index 73558ffce3..d0d0072d15 100644 --- a/docs/internals/builtins/filesystem/stat.md +++ b/docs/internals/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat() — internals" description: "Compiler internals for stat(): lowering path, type checks, and runtime helpers." sidebar: - order: 151 + order: 158 --- ## `stat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/symlink.md b/docs/internals/builtins/filesystem/symlink.md index 903beded32..2df8189372 100644 --- a/docs/internals/builtins/filesystem/symlink.md +++ b/docs/internals/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink() — internals" description: "Compiler internals for symlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 152 + order: 159 --- ## `symlink()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/symlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/sys_get_temp_dir.md b/docs/internals/builtins/filesystem/sys_get_temp_dir.md index fd9f530cb4..b48ca55c46 100644 --- a/docs/internals/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/internals/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir() — internals" description: "Compiler internals for sys_get_temp_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 153 + order: 160 --- ## `sys_get_temp_dir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/sys_get_temp_dir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/tempnam.md b/docs/internals/builtins/filesystem/tempnam.md index 8d11b94ddf..3f85b8d0ed 100644 --- a/docs/internals/builtins/filesystem/tempnam.md +++ b/docs/internals/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam() — internals" description: "Compiler internals for tempnam(): lowering path, type checks, and runtime helpers." sidebar: - order: 154 + order: 161 --- ## `tempnam()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tempnam.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/tmpfile.md b/docs/internals/builtins/filesystem/tmpfile.md index 77ab063ffe..2d7825ed75 100644 --- a/docs/internals/builtins/filesystem/tmpfile.md +++ b/docs/internals/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile() — internals" description: "Compiler internals for tmpfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 155 + order: 162 --- ## `tmpfile()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/tmpfile.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/touch.md b/docs/internals/builtins/filesystem/touch.md index 9732b81524..96712c2d97 100644 --- a/docs/internals/builtins/filesystem/touch.md +++ b/docs/internals/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch() — internals" description: "Compiler internals for touch(): lowering path, type checks, and runtime helpers." sidebar: - order: 156 + order: 163 --- ## `touch()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/touch.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/umask.md b/docs/internals/builtins/filesystem/umask.md index 8de5698434..4117a399f7 100644 --- a/docs/internals/builtins/filesystem/umask.md +++ b/docs/internals/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask() — internals" description: "Compiler internals for umask(): lowering path, type checks, and runtime helpers." sidebar: - order: 157 + order: 164 --- ## `umask()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/umask.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/filesystem/unlink.md b/docs/internals/builtins/filesystem/unlink.md index 8f147789f5..7cd096c4d6 100644 --- a/docs/internals/builtins/filesystem/unlink.md +++ b/docs/internals/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink() — internals" description: "Compiler internals for unlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 158 + order: 165 --- ## `unlink()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/unlink.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/closedir.md b/docs/internals/builtins/io/closedir.md index da60c70a96..913984d84d 100644 --- a/docs/internals/builtins/io/closedir.md +++ b/docs/internals/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir() — internals" description: "Compiler internals for closedir(): lowering path, type checks, and runtime helpers." sidebar: - order: 159 + order: 166 --- ## `closedir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/closedir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fclose.md b/docs/internals/builtins/io/fclose.md index b5a5ea3de7..44ad42abf7 100644 --- a/docs/internals/builtins/io/fclose.md +++ b/docs/internals/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose() — internals" description: "Compiler internals for fclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 160 + order: 167 --- ## `fclose()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fclose.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fdatasync.md b/docs/internals/builtins/io/fdatasync.md index 380f99fbe0..e66c20288e 100644 --- a/docs/internals/builtins/io/fdatasync.md +++ b/docs/internals/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync() — internals" description: "Compiler internals for fdatasync(): lowering path, type checks, and runtime helpers." sidebar: - order: 161 + order: 168 --- ## `fdatasync()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fdatasync.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/feof.md b/docs/internals/builtins/io/feof.md index bb607821c3..aef47db7ed 100644 --- a/docs/internals/builtins/io/feof.md +++ b/docs/internals/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof() — internals" description: "Compiler internals for feof(): lowering path, type checks, and runtime helpers." sidebar: - order: 162 + order: 169 --- ## `feof()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/feof.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fflush.md b/docs/internals/builtins/io/fflush.md index 4f02122124..a852204eca 100644 --- a/docs/internals/builtins/io/fflush.md +++ b/docs/internals/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush() — internals" description: "Compiler internals for fflush(): lowering path, type checks, and runtime helpers." sidebar: - order: 163 + order: 170 --- ## `fflush()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fflush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgetc.md b/docs/internals/builtins/io/fgetc.md index 361eabd674..7118082a47 100644 --- a/docs/internals/builtins/io/fgetc.md +++ b/docs/internals/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc() — internals" description: "Compiler internals for fgetc(): lowering path, type checks, and runtime helpers." sidebar: - order: 164 + order: 171 --- ## `fgetc()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetc.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgetcsv.md b/docs/internals/builtins/io/fgetcsv.md index b4168b15c2..9f8bcf586e 100644 --- a/docs/internals/builtins/io/fgetcsv.md +++ b/docs/internals/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv() — internals" description: "Compiler internals for fgetcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 165 + order: 172 --- ## `fgetcsv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgetcsv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fgets.md b/docs/internals/builtins/io/fgets.md index 652c974b5a..b757bdc5b6 100644 --- a/docs/internals/builtins/io/fgets.md +++ b/docs/internals/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets() — internals" description: "Compiler internals for fgets(): lowering path, type checks, and runtime helpers." sidebar: - order: 166 + order: 173 --- ## `fgets()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fgets.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/file.md b/docs/internals/builtins/io/file.md index d159c76bb1..4434595d78 100644 --- a/docs/internals/builtins/io/file.md +++ b/docs/internals/builtins/io/file.md @@ -2,7 +2,7 @@ title: "file() — internals" description: "Compiler internals for file(): lowering path, type checks, and runtime helpers." sidebar: - order: 167 + order: 174 --- ## `file()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function file(string $filename): array +function file(string $filename, int $flags = 0): array ``` ## What the type checker enforces -- **Arity**: takes exactly 1 argument. +- **Arity**: takes 1–2 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/io/file_get_contents.md b/docs/internals/builtins/io/file_get_contents.md index 58a98f368e..3dae1a6cb6 100644 --- a/docs/internals/builtins/io/file_get_contents.md +++ b/docs/internals/builtins/io/file_get_contents.md @@ -2,7 +2,7 @@ title: "file_get_contents() — internals" description: "Compiler internals for file_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 168 + order: 175 --- ## `file_get_contents()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function file_get_contents(string $filename): mixed +function file_get_contents(string $filename, bool $use_include_path = false, mixed $context = null, int $offset = 0, int $length = null): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 1 argument. +- **Arity**: takes 1–5 arguments (4 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/io/file_put_contents.md b/docs/internals/builtins/io/file_put_contents.md index af6e70c3f0..684c42d3b0 100644 --- a/docs/internals/builtins/io/file_put_contents.md +++ b/docs/internals/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents() — internals" description: "Compiler internals for file_put_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 169 + order: 176 --- ## `file_put_contents()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/file_put_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/flock.md b/docs/internals/builtins/io/flock.md index 3a57b9b684..326d145f9e 100644 --- a/docs/internals/builtins/io/flock.md +++ b/docs/internals/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock() — internals" description: "Compiler internals for flock(): lowering path, type checks, and runtime helpers." sidebar: - order: 170 + order: 177 --- ## `flock()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/flock.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fopen.md b/docs/internals/builtins/io/fopen.md index 814e9e365c..5e4f1781fd 100644 --- a/docs/internals/builtins/io/fopen.md +++ b/docs/internals/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen() — internals" description: "Compiler internals for fopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 171 + order: 178 --- ## `fopen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fpassthru.md b/docs/internals/builtins/io/fpassthru.md index 8cb5c9bdf3..1c73810204 100644 --- a/docs/internals/builtins/io/fpassthru.md +++ b/docs/internals/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru() — internals" description: "Compiler internals for fpassthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 172 + order: 179 --- ## `fpassthru()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fpassthru.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fprintf.md b/docs/internals/builtins/io/fprintf.md index b9d5069ae1..bb71a1780f 100644 --- a/docs/internals/builtins/io/fprintf.md +++ b/docs/internals/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf() — internals" description: "Compiler internals for fprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 173 + order: 180 --- ## `fprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fputcsv.md b/docs/internals/builtins/io/fputcsv.md index 0aea5610da..a4a217136f 100644 --- a/docs/internals/builtins/io/fputcsv.md +++ b/docs/internals/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv() — internals" description: "Compiler internals for fputcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 174 + order: 181 --- ## `fputcsv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fputcsv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fread.md b/docs/internals/builtins/io/fread.md index e758a14459..6513f1c748 100644 --- a/docs/internals/builtins/io/fread.md +++ b/docs/internals/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread() — internals" description: "Compiler internals for fread(): lowering path, type checks, and runtime helpers." sidebar: - order: 175 + order: 182 --- ## `fread()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fread.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fscanf.md b/docs/internals/builtins/io/fscanf.md index 0964559533..d047c7ebbf 100644 --- a/docs/internals/builtins/io/fscanf.md +++ b/docs/internals/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf() — internals" description: "Compiler internals for fscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 176 + order: 183 --- ## `fscanf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fscanf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fseek.md b/docs/internals/builtins/io/fseek.md index 7aab1bd4f5..34d194ddeb 100644 --- a/docs/internals/builtins/io/fseek.md +++ b/docs/internals/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek() — internals" description: "Compiler internals for fseek(): lowering path, type checks, and runtime helpers." sidebar: - order: 177 + order: 184 --- ## `fseek()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fseek.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fstat.md b/docs/internals/builtins/io/fstat.md index 6465de462a..4e68e0fe5a 100644 --- a/docs/internals/builtins/io/fstat.md +++ b/docs/internals/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat() — internals" description: "Compiler internals for fstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 178 + order: 185 --- ## `fstat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fstat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fsync.md b/docs/internals/builtins/io/fsync.md index b21fb58117..31672bc0bd 100644 --- a/docs/internals/builtins/io/fsync.md +++ b/docs/internals/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync() — internals" description: "Compiler internals for fsync(): lowering path, type checks, and runtime helpers." sidebar: - order: 179 + order: 186 --- ## `fsync()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsync.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ftell.md b/docs/internals/builtins/io/ftell.md index 3203ccfe1c..9d0a206a7e 100644 --- a/docs/internals/builtins/io/ftell.md +++ b/docs/internals/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell() — internals" description: "Compiler internals for ftell(): lowering path, type checks, and runtime helpers." sidebar: - order: 180 + order: 187 --- ## `ftell()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftell.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ftruncate.md b/docs/internals/builtins/io/ftruncate.md index 816a807eb4..b29c3bd0b1 100644 --- a/docs/internals/builtins/io/ftruncate.md +++ b/docs/internals/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate() — internals" description: "Compiler internals for ftruncate(): lowering path, type checks, and runtime helpers." sidebar: - order: 181 + order: 188 --- ## `ftruncate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ftruncate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/fwrite.md b/docs/internals/builtins/io/fwrite.md index b3a3a45358..f0f3b18b4f 100644 --- a/docs/internals/builtins/io/fwrite.md +++ b/docs/internals/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite() — internals" description: "Compiler internals for fwrite(): lowering path, type checks, and runtime helpers." sidebar: - order: 182 + order: 189 --- ## `fwrite()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fwrite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostbyaddr.md b/docs/internals/builtins/io/gethostbyaddr.md index 722f5db0ae..3fa6ecb6e1 100644 --- a/docs/internals/builtins/io/gethostbyaddr.md +++ b/docs/internals/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr() — internals" description: "Compiler internals for gethostbyaddr(): lowering path, type checks, and runtime helpers." sidebar: - order: 183 + order: 190 --- ## `gethostbyaddr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyaddr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostbyname.md b/docs/internals/builtins/io/gethostbyname.md index dc3a5d055f..77d5afa507 100644 --- a/docs/internals/builtins/io/gethostbyname.md +++ b/docs/internals/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname() — internals" description: "Compiler internals for gethostbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 184 + order: 191 --- ## `gethostbyname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostbyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/gethostname.md b/docs/internals/builtins/io/gethostname.md index 9767015033..3927ff2ebc 100644 --- a/docs/internals/builtins/io/gethostname.md +++ b/docs/internals/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname() — internals" description: "Compiler internals for gethostname(): lowering path, type checks, and runtime helpers." sidebar: - order: 185 + order: 192 --- ## `gethostname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/gethostname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getprotobyname.md b/docs/internals/builtins/io/getprotobyname.md index d136d78719..f5d34f125d 100644 --- a/docs/internals/builtins/io/getprotobyname.md +++ b/docs/internals/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname() — internals" description: "Compiler internals for getprotobyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 186 + order: 193 --- ## `getprotobyname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getprotobynumber.md b/docs/internals/builtins/io/getprotobynumber.md index 8b70843842..5d03f27831 100644 --- a/docs/internals/builtins/io/getprotobynumber.md +++ b/docs/internals/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber() — internals" description: "Compiler internals for getprotobynumber(): lowering path, type checks, and runtime helpers." sidebar: - order: 187 + order: 194 --- ## `getprotobynumber()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getprotobynumber.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getservbyname.md b/docs/internals/builtins/io/getservbyname.md index 3033fcd358..70bc9e02c9 100644 --- a/docs/internals/builtins/io/getservbyname.md +++ b/docs/internals/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname() — internals" description: "Compiler internals for getservbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 188 + order: 195 --- ## `getservbyname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/getservbyport.md b/docs/internals/builtins/io/getservbyport.md index 16b15b071d..c381762d8d 100644 --- a/docs/internals/builtins/io/getservbyport.md +++ b/docs/internals/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport() — internals" description: "Compiler internals for getservbyport(): lowering path, type checks, and runtime helpers." sidebar: - order: 189 + order: 196 --- ## `getservbyport()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/getservbyport.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/hash_file.md b/docs/internals/builtins/io/hash_file.md index e8ac3a0830..93040e414d 100644 --- a/docs/internals/builtins/io/hash_file.md +++ b/docs/internals/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file() — internals" description: "Compiler internals for hash_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 190 + order: 197 --- ## `hash_file()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/hash_file.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_clean.md b/docs/internals/builtins/io/ob_clean.md index e61edcdfc0..a4c20e4836 100644 --- a/docs/internals/builtins/io/ob_clean.md +++ b/docs/internals/builtins/io/ob_clean.md @@ -2,7 +2,7 @@ title: "ob_clean() — internals" description: "Compiler internals for ob_clean(): lowering path, type checks, and runtime helpers." sidebar: - order: 191 + order: 198 --- ## `ob_clean()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_end_clean.md b/docs/internals/builtins/io/ob_end_clean.md index 0417f05c80..16559da080 100644 --- a/docs/internals/builtins/io/ob_end_clean.md +++ b/docs/internals/builtins/io/ob_end_clean.md @@ -2,7 +2,7 @@ title: "ob_end_clean() — internals" description: "Compiler internals for ob_end_clean(): lowering path, type checks, and runtime helpers." sidebar: - order: 192 + order: 199 --- ## `ob_end_clean()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_end_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_end_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_end_flush.md b/docs/internals/builtins/io/ob_end_flush.md index e32937a6a8..75a64f4d5d 100644 --- a/docs/internals/builtins/io/ob_end_flush.md +++ b/docs/internals/builtins/io/ob_end_flush.md @@ -2,7 +2,7 @@ title: "ob_end_flush() — internals" description: "Compiler internals for ob_end_flush(): lowering path, type checks, and runtime helpers." sidebar: - order: 193 + order: 200 --- ## `ob_end_flush()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_end_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_end_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_flush.md b/docs/internals/builtins/io/ob_flush.md index 7debb7b9b7..83e3412d9c 100644 --- a/docs/internals/builtins/io/ob_flush.md +++ b/docs/internals/builtins/io/ob_flush.md @@ -2,7 +2,7 @@ title: "ob_flush() — internals" description: "Compiler internals for ob_flush(): lowering path, type checks, and runtime helpers." sidebar: - order: 194 + order: 201 --- ## `ob_flush()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_clean.md b/docs/internals/builtins/io/ob_get_clean.md index d11c8d199f..2b305d7cc1 100644 --- a/docs/internals/builtins/io/ob_get_clean.md +++ b/docs/internals/builtins/io/ob_get_clean.md @@ -2,7 +2,7 @@ title: "ob_get_clean() — internals" description: "Compiler internals for ob_get_clean(): lowering path, type checks, and runtime helpers." sidebar: - order: 195 + order: 202 --- ## `ob_get_clean()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_clean.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_clean.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_contents.md b/docs/internals/builtins/io/ob_get_contents.md index cab3744f49..22ec0256c2 100644 --- a/docs/internals/builtins/io/ob_get_contents.md +++ b/docs/internals/builtins/io/ob_get_contents.md @@ -2,7 +2,7 @@ title: "ob_get_contents() — internals" description: "Compiler internals for ob_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 196 + order: 203 --- ## `ob_get_contents()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_flush.md b/docs/internals/builtins/io/ob_get_flush.md index 69bec4fd14..aa20c1146b 100644 --- a/docs/internals/builtins/io/ob_get_flush.md +++ b/docs/internals/builtins/io/ob_get_flush.md @@ -2,7 +2,7 @@ title: "ob_get_flush() — internals" description: "Compiler internals for ob_get_flush(): lowering path, type checks, and runtime helpers." sidebar: - order: 197 + order: 204 --- ## `ob_get_flush()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_length.md b/docs/internals/builtins/io/ob_get_length.md index c3cfbd1371..4494a286d1 100644 --- a/docs/internals/builtins/io/ob_get_length.md +++ b/docs/internals/builtins/io/ob_get_length.md @@ -2,7 +2,7 @@ title: "ob_get_length() — internals" description: "Compiler internals for ob_get_length(): lowering path, type checks, and runtime helpers." sidebar: - order: 198 + order: 205 --- ## `ob_get_length()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_length.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_length.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_level.md b/docs/internals/builtins/io/ob_get_level.md index 8d1a9ff45c..81969cd488 100644 --- a/docs/internals/builtins/io/ob_get_level.md +++ b/docs/internals/builtins/io/ob_get_level.md @@ -2,7 +2,7 @@ title: "ob_get_level() — internals" description: "Compiler internals for ob_get_level(): lowering path, type checks, and runtime helpers." sidebar: - order: 199 + order: 206 --- ## `ob_get_level()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_level.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_level.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_get_status.md b/docs/internals/builtins/io/ob_get_status.md index 96553ae1a1..7812f9ea33 100644 --- a/docs/internals/builtins/io/ob_get_status.md +++ b/docs/internals/builtins/io/ob_get_status.md @@ -2,7 +2,7 @@ title: "ob_get_status() — internals" description: "Compiler internals for ob_get_status(): lowering path, type checks, and runtime helpers." sidebar: - order: 200 + order: 207 --- ## `ob_get_status()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_get_status.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_get_status.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_implicit_flush.md b/docs/internals/builtins/io/ob_implicit_flush.md index 040ab8c4e4..1b601220b3 100644 --- a/docs/internals/builtins/io/ob_implicit_flush.md +++ b/docs/internals/builtins/io/ob_implicit_flush.md @@ -2,7 +2,7 @@ title: "ob_implicit_flush() — internals" description: "Compiler internals for ob_implicit_flush(): lowering path, type checks, and runtime helpers." sidebar: - order: 201 + order: 208 --- ## `ob_implicit_flush()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_implicit_flush.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_implicit_flush.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_list_handlers.md b/docs/internals/builtins/io/ob_list_handlers.md index 5af0c5b065..6219955db4 100644 --- a/docs/internals/builtins/io/ob_list_handlers.md +++ b/docs/internals/builtins/io/ob_list_handlers.md @@ -2,7 +2,7 @@ title: "ob_list_handlers() — internals" description: "Compiler internals for ob_list_handlers(): lowering path, type checks, and runtime helpers." sidebar: - order: 202 + order: 209 --- ## `ob_list_handlers()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_list_handlers.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_list_handlers.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/ob_start.md b/docs/internals/builtins/io/ob_start.md index 9a67971ce0..f4cea1f2fb 100644 --- a/docs/internals/builtins/io/ob_start.md +++ b/docs/internals/builtins/io/ob_start.md @@ -2,7 +2,7 @@ title: "ob_start() — internals" description: "Compiler internals for ob_start(): lowering path, type checks, and runtime helpers." sidebar: - order: 203 + order: 210 --- ## `ob_start()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/ob_start.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/ob_start.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/opendir.md b/docs/internals/builtins/io/opendir.md index 0fb48b969f..a31a2142fe 100644 --- a/docs/internals/builtins/io/opendir.md +++ b/docs/internals/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir() — internals" description: "Compiler internals for opendir(): lowering path, type checks, and runtime helpers." sidebar: - order: 204 + order: 211 --- ## `opendir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/opendir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/readdir.md b/docs/internals/builtins/io/readdir.md index 4d8867a9fe..6c8885f008 100644 --- a/docs/internals/builtins/io/readdir.md +++ b/docs/internals/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir() — internals" description: "Compiler internals for readdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 205 + order: 212 --- ## `readdir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readdir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/rewind.md b/docs/internals/builtins/io/rewind.md index 2d18b12c03..1f00aee026 100644 --- a/docs/internals/builtins/io/rewind.md +++ b/docs/internals/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind() — internals" description: "Compiler internals for rewind(): lowering path, type checks, and runtime helpers." sidebar: - order: 206 + order: 213 --- ## `rewind()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewind.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/rewinddir.md b/docs/internals/builtins/io/rewinddir.md index ba138f8cdc..daf93f7e0d 100644 --- a/docs/internals/builtins/io/rewinddir.md +++ b/docs/internals/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir() — internals" description: "Compiler internals for rewinddir(): lowering path, type checks, and runtime helpers." sidebar: - order: 207 + order: 214 --- ## `rewinddir()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/rewinddir.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_bucket_make_writeable.md b/docs/internals/builtins/io/stream_bucket_make_writeable.md index 00705bd73d..4e79e63c6a 100644 --- a/docs/internals/builtins/io/stream_bucket_make_writeable.md +++ b/docs/internals/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable() — internals" description: "Compiler internals for stream_bucket_make_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 208 + order: 215 --- ## `stream_bucket_make_writeable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_make_writeable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_bucket_new.md b/docs/internals/builtins/io/stream_bucket_new.md index 9811211575..8225b1143e 100644 --- a/docs/internals/builtins/io/stream_bucket_new.md +++ b/docs/internals/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new() — internals" description: "Compiler internals for stream_bucket_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 209 + order: 216 --- ## `stream_bucket_new()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_new.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_create.md b/docs/internals/builtins/io/stream_context_create.md index a4de1267b2..647f246619 100644 --- a/docs/internals/builtins/io/stream_context_create.md +++ b/docs/internals/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create() — internals" description: "Compiler internals for stream_context_create(): lowering path, type checks, and runtime helpers." sidebar: - order: 210 + order: 217 --- ## `stream_context_create()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_create.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_default.md b/docs/internals/builtins/io/stream_context_get_default.md index f0202f1265..de2572ebb3 100644 --- a/docs/internals/builtins/io/stream_context_get_default.md +++ b/docs/internals/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default() — internals" description: "Compiler internals for stream_context_get_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 211 + order: 218 --- ## `stream_context_get_default()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_default.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_options.md b/docs/internals/builtins/io/stream_context_get_options.md index 6ee8761b93..04c9b9e422 100644 --- a/docs/internals/builtins/io/stream_context_get_options.md +++ b/docs/internals/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options() — internals" description: "Compiler internals for stream_context_get_options(): lowering path, type checks, and runtime helpers." sidebar: - order: 212 + order: 219 --- ## `stream_context_get_options()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_options.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_get_params.md b/docs/internals/builtins/io/stream_context_get_params.md index 521790c528..a3e34b8d5b 100644 --- a/docs/internals/builtins/io/stream_context_get_params.md +++ b/docs/internals/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params() — internals" description: "Compiler internals for stream_context_get_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 213 + order: 220 --- ## `stream_context_get_params()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_get_params.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_default.md b/docs/internals/builtins/io/stream_context_set_default.md index b0ede765c0..a153178aca 100644 --- a/docs/internals/builtins/io/stream_context_set_default.md +++ b/docs/internals/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default() — internals" description: "Compiler internals for stream_context_set_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 214 + order: 221 --- ## `stream_context_set_default()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_default.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_option.md b/docs/internals/builtins/io/stream_context_set_option.md index fcfed79925..4375522d4b 100644 --- a/docs/internals/builtins/io/stream_context_set_option.md +++ b/docs/internals/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option() — internals" description: "Compiler internals for stream_context_set_option(): lowering path, type checks, and runtime helpers." sidebar: - order: 215 + order: 222 --- ## `stream_context_set_option()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_option.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_context_set_params.md b/docs/internals/builtins/io/stream_context_set_params.md index e73a7fa983..f0173a36f6 100644 --- a/docs/internals/builtins/io/stream_context_set_params.md +++ b/docs/internals/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params() — internals" description: "Compiler internals for stream_context_set_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 216 + order: 223 --- ## `stream_context_set_params()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_context_set_params.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_copy_to_stream.md b/docs/internals/builtins/io/stream_copy_to_stream.md index 3dc073435a..2441a2327a 100644 --- a/docs/internals/builtins/io/stream_copy_to_stream.md +++ b/docs/internals/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream() — internals" description: "Compiler internals for stream_copy_to_stream(): lowering path, type checks, and runtime helpers." sidebar: - order: 217 + order: 224 --- ## `stream_copy_to_stream()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_copy_to_stream.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_filter_register.md b/docs/internals/builtins/io/stream_filter_register.md index c2c13c43c1..904b7aa661 100644 --- a/docs/internals/builtins/io/stream_filter_register.md +++ b/docs/internals/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register() — internals" description: "Compiler internals for stream_filter_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 218 + order: 225 --- ## `stream_filter_register()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_filter_remove.md b/docs/internals/builtins/io/stream_filter_remove.md index 651aca90b1..08f75ac30a 100644 --- a/docs/internals/builtins/io/stream_filter_remove.md +++ b/docs/internals/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove() — internals" description: "Compiler internals for stream_filter_remove(): lowering path, type checks, and runtime helpers." sidebar: - order: 219 + order: 226 --- ## `stream_filter_remove()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_remove.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_contents.md b/docs/internals/builtins/io/stream_get_contents.md index 8780001e4a..0f732f4769 100644 --- a/docs/internals/builtins/io/stream_get_contents.md +++ b/docs/internals/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents() — internals" description: "Compiler internals for stream_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 220 + order: 227 --- ## `stream_get_contents()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_contents.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_filters.md b/docs/internals/builtins/io/stream_get_filters.md index c4df05f70e..64d28e1845 100644 --- a/docs/internals/builtins/io/stream_get_filters.md +++ b/docs/internals/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters() — internals" description: "Compiler internals for stream_get_filters(): lowering path, type checks, and runtime helpers." sidebar: - order: 221 + order: 228 --- ## `stream_get_filters()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_filters.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_line.md b/docs/internals/builtins/io/stream_get_line.md index 31854ab9ad..c9f66e781b 100644 --- a/docs/internals/builtins/io/stream_get_line.md +++ b/docs/internals/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line() — internals" description: "Compiler internals for stream_get_line(): lowering path, type checks, and runtime helpers." sidebar: - order: 222 + order: 229 --- ## `stream_get_line()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_line.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_meta_data.md b/docs/internals/builtins/io/stream_get_meta_data.md index 8177a83754..b4f472c33f 100644 --- a/docs/internals/builtins/io/stream_get_meta_data.md +++ b/docs/internals/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data() — internals" description: "Compiler internals for stream_get_meta_data(): lowering path, type checks, and runtime helpers." sidebar: - order: 223 + order: 230 --- ## `stream_get_meta_data()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_meta_data.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_transports.md b/docs/internals/builtins/io/stream_get_transports.md index 4f5af37508..d4c3da1534 100644 --- a/docs/internals/builtins/io/stream_get_transports.md +++ b/docs/internals/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports() — internals" description: "Compiler internals for stream_get_transports(): lowering path, type checks, and runtime helpers." sidebar: - order: 224 + order: 231 --- ## `stream_get_transports()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_transports.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_get_wrappers.md b/docs/internals/builtins/io/stream_get_wrappers.md index a8d852ce43..80a871e779 100644 --- a/docs/internals/builtins/io/stream_get_wrappers.md +++ b/docs/internals/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers() — internals" description: "Compiler internals for stream_get_wrappers(): lowering path, type checks, and runtime helpers." sidebar: - order: 225 + order: 232 --- ## `stream_get_wrappers()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_get_wrappers.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_is_local.md b/docs/internals/builtins/io/stream_is_local.md index 5f918a045c..d434b3dfc5 100644 --- a/docs/internals/builtins/io/stream_is_local.md +++ b/docs/internals/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local() — internals" description: "Compiler internals for stream_is_local(): lowering path, type checks, and runtime helpers." sidebar: - order: 226 + order: 233 --- ## `stream_is_local()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_is_local.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_isatty.md b/docs/internals/builtins/io/stream_isatty.md index e8c01b08ac..b38e1f9017 100644 --- a/docs/internals/builtins/io/stream_isatty.md +++ b/docs/internals/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty() — internals" description: "Compiler internals for stream_isatty(): lowering path, type checks, and runtime helpers." sidebar: - order: 227 + order: 234 --- ## `stream_isatty()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_isatty.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_resolve_include_path.md b/docs/internals/builtins/io/stream_resolve_include_path.md index 6350181b0c..7ad41ce4dd 100644 --- a/docs/internals/builtins/io/stream_resolve_include_path.md +++ b/docs/internals/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path() — internals" description: "Compiler internals for stream_resolve_include_path(): lowering path, type checks, and runtime helpers." sidebar: - order: 228 + order: 235 --- ## `stream_resolve_include_path()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_resolve_include_path.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_select.md b/docs/internals/builtins/io/stream_select.md index d813bd146b..5fc5d7b166 100644 --- a/docs/internals/builtins/io/stream_select.md +++ b/docs/internals/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select() — internals" description: "Compiler internals for stream_select(): lowering path, type checks, and runtime helpers." sidebar: - order: 229 + order: 236 --- ## `stream_select()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_select.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_blocking.md b/docs/internals/builtins/io/stream_set_blocking.md index 79c229d5eb..a13dd5ea9a 100644 --- a/docs/internals/builtins/io/stream_set_blocking.md +++ b/docs/internals/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking() — internals" description: "Compiler internals for stream_set_blocking(): lowering path, type checks, and runtime helpers." sidebar: - order: 230 + order: 237 --- ## `stream_set_blocking()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_blocking.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_chunk_size.md b/docs/internals/builtins/io/stream_set_chunk_size.md index 0847673e91..0a7ac2a5a2 100644 --- a/docs/internals/builtins/io/stream_set_chunk_size.md +++ b/docs/internals/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size() — internals" description: "Compiler internals for stream_set_chunk_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 231 + order: 238 --- ## `stream_set_chunk_size()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_chunk_size.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_read_buffer.md b/docs/internals/builtins/io/stream_set_read_buffer.md index 4fcbb9d33c..8bd65f8365 100644 --- a/docs/internals/builtins/io/stream_set_read_buffer.md +++ b/docs/internals/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer() — internals" description: "Compiler internals for stream_set_read_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 232 + order: 239 --- ## `stream_set_read_buffer()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_read_buffer.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_timeout.md b/docs/internals/builtins/io/stream_set_timeout.md index 5a824aefa0..d17bcca51f 100644 --- a/docs/internals/builtins/io/stream_set_timeout.md +++ b/docs/internals/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout() — internals" description: "Compiler internals for stream_set_timeout(): lowering path, type checks, and runtime helpers." sidebar: - order: 233 + order: 240 --- ## `stream_set_timeout()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_timeout.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_set_write_buffer.md b/docs/internals/builtins/io/stream_set_write_buffer.md index 77050fbdf5..90ac7e809f 100644 --- a/docs/internals/builtins/io/stream_set_write_buffer.md +++ b/docs/internals/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer() — internals" description: "Compiler internals for stream_set_write_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 234 + order: 241 --- ## `stream_set_write_buffer()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_set_write_buffer.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_accept.md b/docs/internals/builtins/io/stream_socket_accept.md index 50391d04cf..38a09770fa 100644 --- a/docs/internals/builtins/io/stream_socket_accept.md +++ b/docs/internals/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept() — internals" description: "Compiler internals for stream_socket_accept(): lowering path, type checks, and runtime helpers." sidebar: - order: 235 + order: 242 --- ## `stream_socket_accept()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_accept.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_client.md b/docs/internals/builtins/io/stream_socket_client.md index cf89fc5af3..9092760aaa 100644 --- a/docs/internals/builtins/io/stream_socket_client.md +++ b/docs/internals/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client() — internals" description: "Compiler internals for stream_socket_client(): lowering path, type checks, and runtime helpers." sidebar: - order: 236 + order: 243 --- ## `stream_socket_client()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_client.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_enable_crypto.md b/docs/internals/builtins/io/stream_socket_enable_crypto.md index df1d82ad39..90f4a717b7 100644 --- a/docs/internals/builtins/io/stream_socket_enable_crypto.md +++ b/docs/internals/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto() — internals" description: "Compiler internals for stream_socket_enable_crypto(): lowering path, type checks, and runtime helpers." sidebar: - order: 237 + order: 244 --- ## `stream_socket_enable_crypto()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_enable_crypto.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_get_name.md b/docs/internals/builtins/io/stream_socket_get_name.md index 442c4324db..93b82cd03b 100644 --- a/docs/internals/builtins/io/stream_socket_get_name.md +++ b/docs/internals/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name() — internals" description: "Compiler internals for stream_socket_get_name(): lowering path, type checks, and runtime helpers." sidebar: - order: 238 + order: 245 --- ## `stream_socket_get_name()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_get_name.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_pair.md b/docs/internals/builtins/io/stream_socket_pair.md index b2339bab34..2727331275 100644 --- a/docs/internals/builtins/io/stream_socket_pair.md +++ b/docs/internals/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair() — internals" description: "Compiler internals for stream_socket_pair(): lowering path, type checks, and runtime helpers." sidebar: - order: 239 + order: 246 --- ## `stream_socket_pair()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_pair.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_recvfrom.md b/docs/internals/builtins/io/stream_socket_recvfrom.md index 67756ef5e0..392915dff7 100644 --- a/docs/internals/builtins/io/stream_socket_recvfrom.md +++ b/docs/internals/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom() — internals" description: "Compiler internals for stream_socket_recvfrom(): lowering path, type checks, and runtime helpers." sidebar: - order: 240 + order: 247 --- ## `stream_socket_recvfrom()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_recvfrom.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_sendto.md b/docs/internals/builtins/io/stream_socket_sendto.md index 99133f9a8e..1b8e422d85 100644 --- a/docs/internals/builtins/io/stream_socket_sendto.md +++ b/docs/internals/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto() — internals" description: "Compiler internals for stream_socket_sendto(): lowering path, type checks, and runtime helpers." sidebar: - order: 241 + order: 248 --- ## `stream_socket_sendto()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_sendto.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_server.md b/docs/internals/builtins/io/stream_socket_server.md index 7d97df0aee..26d2dc65ac 100644 --- a/docs/internals/builtins/io/stream_socket_server.md +++ b/docs/internals/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server() — internals" description: "Compiler internals for stream_socket_server(): lowering path, type checks, and runtime helpers." sidebar: - order: 242 + order: 249 --- ## `stream_socket_server()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_server.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_socket_shutdown.md b/docs/internals/builtins/io/stream_socket_shutdown.md index 445c9f3361..dc62aaf7c5 100644 --- a/docs/internals/builtins/io/stream_socket_shutdown.md +++ b/docs/internals/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown() — internals" description: "Compiler internals for stream_socket_shutdown(): lowering path, type checks, and runtime helpers." sidebar: - order: 243 + order: 250 --- ## `stream_socket_shutdown()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_socket_shutdown.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_supports_lock.md b/docs/internals/builtins/io/stream_supports_lock.md index c3546094ef..45a997eba3 100644 --- a/docs/internals/builtins/io/stream_supports_lock.md +++ b/docs/internals/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock() — internals" description: "Compiler internals for stream_supports_lock(): lowering path, type checks, and runtime helpers." sidebar: - order: 244 + order: 251 --- ## `stream_supports_lock()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_supports_lock.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_register.md b/docs/internals/builtins/io/stream_wrapper_register.md index 40e05659ce..322435f67a 100644 --- a/docs/internals/builtins/io/stream_wrapper_register.md +++ b/docs/internals/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register() — internals" description: "Compiler internals for stream_wrapper_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 245 + order: 252 --- ## `stream_wrapper_register()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_restore.md b/docs/internals/builtins/io/stream_wrapper_restore.md index 05a47e9338..deec4487f5 100644 --- a/docs/internals/builtins/io/stream_wrapper_restore.md +++ b/docs/internals/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore() — internals" description: "Compiler internals for stream_wrapper_restore(): lowering path, type checks, and runtime helpers." sidebar: - order: 246 + order: 253 --- ## `stream_wrapper_restore()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_restore.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/stream_wrapper_unregister.md b/docs/internals/builtins/io/stream_wrapper_unregister.md index 927ecf3e4b..ef0045be30 100644 --- a/docs/internals/builtins/io/stream_wrapper_unregister.md +++ b/docs/internals/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister() — internals" description: "Compiler internals for stream_wrapper_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 247 + order: 254 --- ## `stream_wrapper_unregister()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_wrapper_unregister.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/io/vfprintf.md b/docs/internals/builtins/io/vfprintf.md index af61e9760c..7cc62f5c75 100644 --- a/docs/internals/builtins/io/vfprintf.md +++ b/docs/internals/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf() — internals" description: "Compiler internals for vfprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 248 + order: 255 --- ## `vfprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/vfprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_decode.md b/docs/internals/builtins/json/json_decode.md index 8f22ce9301..1064421a63 100644 --- a/docs/internals/builtins/json/json_decode.md +++ b/docs/internals/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode() — internals" description: "Compiler internals for json_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 249 + order: 256 --- ## `json_decode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_encode.md b/docs/internals/builtins/json/json_encode.md index ee982d0ab1..b197dfd518 100644 --- a/docs/internals/builtins/json/json_encode.md +++ b/docs/internals/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode() — internals" description: "Compiler internals for json_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 250 + order: 257 --- ## `json_encode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_encode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_last_error.md b/docs/internals/builtins/json/json_last_error.md index 7d159e3311..e8bb61757d 100644 --- a/docs/internals/builtins/json/json_last_error.md +++ b/docs/internals/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error() — internals" description: "Compiler internals for json_last_error(): lowering path, type checks, and runtime helpers." sidebar: - order: 251 + order: 258 --- ## `json_last_error()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_last_error.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_last_error_msg.md b/docs/internals/builtins/json/json_last_error_msg.md index 835737b286..28446b06a4 100644 --- a/docs/internals/builtins/json/json_last_error_msg.md +++ b/docs/internals/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg() — internals" description: "Compiler internals for json_last_error_msg(): lowering path, type checks, and runtime helpers." sidebar: - order: 252 + order: 259 --- ## `json_last_error_msg()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_last_error_msg.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/json/json_validate.md b/docs/internals/builtins/json/json_validate.md index 32cd606d28..e087f5f3bc 100644 --- a/docs/internals/builtins/json/json_validate.md +++ b/docs/internals/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate() — internals" description: "Compiler internals for json_validate(): lowering path, type checks, and runtime helpers." sidebar: - order: 253 + order: 260 --- ## `json_validate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/json_validate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/abs.md b/docs/internals/builtins/math/abs.md index d586168eff..f5a664bf06 100644 --- a/docs/internals/builtins/math/abs.md +++ b/docs/internals/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs() — internals" description: "Compiler internals for abs(): lowering path, type checks, and runtime helpers." sidebar: - order: 254 + order: 261 --- ## `abs()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/abs.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/acos.md b/docs/internals/builtins/math/acos.md index a1a5b47dc5..35814bfa78 100644 --- a/docs/internals/builtins/math/acos.md +++ b/docs/internals/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos() — internals" description: "Compiler internals for acos(): lowering path, type checks, and runtime helpers." sidebar: - order: 255 + order: 262 --- ## `acos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/acos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/asin.md b/docs/internals/builtins/math/asin.md index aca374a0b2..5422ea68ea 100644 --- a/docs/internals/builtins/math/asin.md +++ b/docs/internals/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin() — internals" description: "Compiler internals for asin(): lowering path, type checks, and runtime helpers." sidebar: - order: 256 + order: 263 --- ## `asin()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/asin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/atan.md b/docs/internals/builtins/math/atan.md index 93025f34b1..51338fb078 100644 --- a/docs/internals/builtins/math/atan.md +++ b/docs/internals/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan() — internals" description: "Compiler internals for atan(): lowering path, type checks, and runtime helpers." sidebar: - order: 257 + order: 264 --- ## `atan()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/atan2.md b/docs/internals/builtins/math/atan2.md index ccf1873be2..c4fbb74e27 100644 --- a/docs/internals/builtins/math/atan2.md +++ b/docs/internals/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2() — internals" description: "Compiler internals for atan2(): lowering path, type checks, and runtime helpers." sidebar: - order: 258 + order: 265 --- ## `atan2()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/atan2.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/base_convert.md b/docs/internals/builtins/math/base_convert.md new file mode 100644 index 0000000000..8328408583 --- /dev/null +++ b/docs/internals/builtins/math/base_convert.md @@ -0,0 +1,56 @@ +--- +title: "base_convert() — internals" +description: "Compiler internals for base_convert(): lowering path, type checks, and runtime helpers." +sidebar: + order: 266 +--- + +## `base_convert()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/base_convert.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/base_convert.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.base_convert` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.base_convert` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function base_convert(string $num, int $from_base, int $to_base): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 3 arguments. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `base_convert()`](../../../php/builtins/math/base_convert.md) diff --git a/docs/internals/builtins/math/bindec.md b/docs/internals/builtins/math/bindec.md new file mode 100644 index 0000000000..d923bcfb26 --- /dev/null +++ b/docs/internals/builtins/math/bindec.md @@ -0,0 +1,55 @@ +--- +title: "bindec() — internals" +description: "Compiler internals for bindec(): lowering path, type checks, and runtime helpers." +sidebar: + order: 267 +--- + +## `bindec()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/bindec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/bindec.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.bindec` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.bindec` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function bindec(string $binary_string): mixed +``` + +## 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 + +- [User reference for `bindec()`](../../../php/builtins/math/bindec.md) diff --git a/docs/internals/builtins/math/ceil.md b/docs/internals/builtins/math/ceil.md index 6e2ae8c48b..ae826e28d1 100644 --- a/docs/internals/builtins/math/ceil.md +++ b/docs/internals/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil() — internals" description: "Compiler internals for ceil(): lowering path, type checks, and runtime helpers." sidebar: - order: 259 + order: 268 --- ## `ceil()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/ceil.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/clamp.md b/docs/internals/builtins/math/clamp.md index 864d5ceeba..fdc4d33351 100644 --- a/docs/internals/builtins/math/clamp.md +++ b/docs/internals/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp() — internals" description: "Compiler internals for clamp(): lowering path, type checks, and runtime helpers." sidebar: - order: 260 + order: 269 --- ## `clamp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/clamp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/cos.md b/docs/internals/builtins/math/cos.md index efdeb267a5..1af4329c65 100644 --- a/docs/internals/builtins/math/cos.md +++ b/docs/internals/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos() — internals" description: "Compiler internals for cos(): lowering path, type checks, and runtime helpers." sidebar: - order: 261 + order: 270 --- ## `cos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/cosh.md b/docs/internals/builtins/math/cosh.md index 4abd14de4f..f2f2536f47 100644 --- a/docs/internals/builtins/math/cosh.md +++ b/docs/internals/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh() — internals" description: "Compiler internals for cosh(): lowering path, type checks, and runtime helpers." sidebar: - order: 262 + order: 271 --- ## `cosh()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/cosh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/decbin.md b/docs/internals/builtins/math/decbin.md new file mode 100644 index 0000000000..d36341519c --- /dev/null +++ b/docs/internals/builtins/math/decbin.md @@ -0,0 +1,55 @@ +--- +title: "decbin() — internals" +description: "Compiler internals for decbin(): lowering path, type checks, and runtime helpers." +sidebar: + order: 272 +--- + +## `decbin()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/decbin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/decbin.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.decbin` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.decbin` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function decbin(int $num): string +``` + +## 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 + +- [User reference for `decbin()`](../../../php/builtins/math/decbin.md) diff --git a/docs/internals/builtins/math/dechex.md b/docs/internals/builtins/math/dechex.md new file mode 100644 index 0000000000..61a6a11ead --- /dev/null +++ b/docs/internals/builtins/math/dechex.md @@ -0,0 +1,55 @@ +--- +title: "dechex() — internals" +description: "Compiler internals for dechex(): lowering path, type checks, and runtime helpers." +sidebar: + order: 273 +--- + +## `dechex()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/dechex.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/dechex.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.dechex` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.dechex` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function dechex(int $num): string +``` + +## 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 + +- [User reference for `dechex()`](../../../php/builtins/math/dechex.md) diff --git a/docs/internals/builtins/math/decoct.md b/docs/internals/builtins/math/decoct.md new file mode 100644 index 0000000000..4f63000e12 --- /dev/null +++ b/docs/internals/builtins/math/decoct.md @@ -0,0 +1,55 @@ +--- +title: "decoct() — internals" +description: "Compiler internals for decoct(): lowering path, type checks, and runtime helpers." +sidebar: + order: 274 +--- + +## `decoct()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/decoct.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/decoct.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.decoct` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.decoct` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function decoct(int $num): string +``` + +## 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 + +- [User reference for `decoct()`](../../../php/builtins/math/decoct.md) diff --git a/docs/internals/builtins/math/deg2rad.md b/docs/internals/builtins/math/deg2rad.md index 75857e8486..54ea1b4559 100644 --- a/docs/internals/builtins/math/deg2rad.md +++ b/docs/internals/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad() — internals" description: "Compiler internals for deg2rad(): lowering path, type checks, and runtime helpers." sidebar: - order: 263 + order: 275 --- ## `deg2rad()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/deg2rad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/exp.md b/docs/internals/builtins/math/exp.md index 046026a40b..ea5f74ab5e 100644 --- a/docs/internals/builtins/math/exp.md +++ b/docs/internals/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp() — internals" description: "Compiler internals for exp(): lowering path, type checks, and runtime helpers." sidebar: - order: 264 + order: 276 --- ## `exp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/exp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/fdiv.md b/docs/internals/builtins/math/fdiv.md index 2d8a4f5b4e..53111a8ea5 100644 --- a/docs/internals/builtins/math/fdiv.md +++ b/docs/internals/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv() — internals" description: "Compiler internals for fdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 265 + order: 277 --- ## `fdiv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fdiv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/floor.md b/docs/internals/builtins/math/floor.md index 659b8e8972..6d5ae6e4d4 100644 --- a/docs/internals/builtins/math/floor.md +++ b/docs/internals/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor() — internals" description: "Compiler internals for floor(): lowering path, type checks, and runtime helpers." sidebar: - order: 266 + order: 278 --- ## `floor()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/floor.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/fmod.md b/docs/internals/builtins/math/fmod.md index fa8548bceb..94aa5aa32c 100644 --- a/docs/internals/builtins/math/fmod.md +++ b/docs/internals/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod() — internals" description: "Compiler internals for fmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 267 + order: 279 --- ## `fmod()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/fmod.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/hexdec.md b/docs/internals/builtins/math/hexdec.md new file mode 100644 index 0000000000..a44f0ae23e --- /dev/null +++ b/docs/internals/builtins/math/hexdec.md @@ -0,0 +1,55 @@ +--- +title: "hexdec() — internals" +description: "Compiler internals for hexdec(): lowering path, type checks, and runtime helpers." +sidebar: + order: 280 +--- + +## `hexdec()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/hexdec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/hexdec.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.hexdec` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.hexdec` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function hexdec(string $hex_string): mixed +``` + +## 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 + +- [User reference for `hexdec()`](../../../php/builtins/math/hexdec.md) diff --git a/docs/internals/builtins/math/hypot.md b/docs/internals/builtins/math/hypot.md index 92be0076a0..98cc05e117 100644 --- a/docs/internals/builtins/math/hypot.md +++ b/docs/internals/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot() — internals" description: "Compiler internals for hypot(): lowering path, type checks, and runtime helpers." sidebar: - order: 268 + order: 281 --- ## `hypot()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/hypot.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/intdiv.md b/docs/internals/builtins/math/intdiv.md index ac85b449c6..7babb89fde 100644 --- a/docs/internals/builtins/math/intdiv.md +++ b/docs/internals/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv() — internals" description: "Compiler internals for intdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 269 + order: 282 --- ## `intdiv()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/intdiv.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_finite.md b/docs/internals/builtins/math/is_finite.md index 58a70b0fd7..18bbc83855 100644 --- a/docs/internals/builtins/math/is_finite.md +++ b/docs/internals/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite() — internals" description: "Compiler internals for is_finite(): lowering path, type checks, and runtime helpers." sidebar: - order: 270 + order: 283 --- ## `is_finite()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_finite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_infinite.md b/docs/internals/builtins/math/is_infinite.md index 2b658dd74a..6d89a831e9 100644 --- a/docs/internals/builtins/math/is_infinite.md +++ b/docs/internals/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite() — internals" description: "Compiler internals for is_infinite(): lowering path, type checks, and runtime helpers." sidebar: - order: 271 + order: 284 --- ## `is_infinite()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_infinite.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/is_nan.md b/docs/internals/builtins/math/is_nan.md index f8d7b78917..904dd26e25 100644 --- a/docs/internals/builtins/math/is_nan.md +++ b/docs/internals/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan() — internals" description: "Compiler internals for is_nan(): lowering path, type checks, and runtime helpers." sidebar: - order: 272 + order: 285 --- ## `is_nan()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_nan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log.md b/docs/internals/builtins/math/log.md index 05a6464290..ae5cb4dcb7 100644 --- a/docs/internals/builtins/math/log.md +++ b/docs/internals/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log() — internals" description: "Compiler internals for log(): lowering path, type checks, and runtime helpers." sidebar: - order: 273 + order: 286 --- ## `log()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log10.md b/docs/internals/builtins/math/log10.md index a78a6a97ca..32effebc3d 100644 --- a/docs/internals/builtins/math/log10.md +++ b/docs/internals/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10() — internals" description: "Compiler internals for log10(): lowering path, type checks, and runtime helpers." sidebar: - order: 274 + order: 287 --- ## `log10()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log10.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/log2.md b/docs/internals/builtins/math/log2.md index 34cb11c67c..e94b713167 100644 --- a/docs/internals/builtins/math/log2.md +++ b/docs/internals/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2() — internals" description: "Compiler internals for log2(): lowering path, type checks, and runtime helpers." sidebar: - order: 275 + order: 288 --- ## `log2()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/log2.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/max.md b/docs/internals/builtins/math/max.md index ac60b35d3f..685e1bd625 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max() — internals" description: "Compiler internals for max(): lowering path, type checks, and runtime helpers." sidebar: - order: 276 + order: 289 --- ## `max()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/max.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -25,8 +25,8 @@ sidebar: - **Target strategy**: `runtime_call` - **Validation**: `checker_hook` - **Result type source**: `checked` -- **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/math/min.md b/docs/internals/builtins/math/min.md index 5b0f8b1ac2..bcb65a1a22 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min() — internals" description: "Compiler internals for min(): lowering path, type checks, and runtime helpers." sidebar: - order: 277 + order: 290 --- ## `min()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/min.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -25,8 +25,8 @@ sidebar: - **Target strategy**: `runtime_call` - **Validation**: `checker_hook` - **Result type source**: `checked` -- **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/math/mt_rand.md b/docs/internals/builtins/math/mt_rand.md index e6aa9c2bfd..ef9f4cba65 100644 --- a/docs/internals/builtins/math/mt_rand.md +++ b/docs/internals/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand() — internals" description: "Compiler internals for mt_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 278 + order: 291 --- ## `mt_rand()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/mt_rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (2 declared effects)` +- **Effects**: `static (3 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/math/octdec.md b/docs/internals/builtins/math/octdec.md new file mode 100644 index 0000000000..dd7500bf5d --- /dev/null +++ b/docs/internals/builtins/math/octdec.md @@ -0,0 +1,55 @@ +--- +title: "octdec() — internals" +description: "Compiler internals for octdec(): lowering path, type checks, and runtime helpers." +sidebar: + order: 292 +--- + +## `octdec()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/math/octdec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/octdec.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.octdec` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.octdec` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function octdec(string $octal_string): mixed +``` + +## 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 + +- [User reference for `octdec()`](../../../php/builtins/math/octdec.md) diff --git a/docs/internals/builtins/math/pi.md b/docs/internals/builtins/math/pi.md index e9ebcd8f89..fdd09eae1c 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi() — internals" description: "Compiler internals for pi(): lowering path, type checks, and runtime helpers." sidebar: - order: 279 + order: 293 --- ## `pi()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pi.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/pow.md b/docs/internals/builtins/math/pow.md index d448dbea4c..6659752554 100644 --- a/docs/internals/builtins/math/pow.md +++ b/docs/internals/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow() — internals" description: "Compiler internals for pow(): lowering path, type checks, and runtime helpers." sidebar: - order: 280 + order: 294 --- ## `pow()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/pow.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/rad2deg.md b/docs/internals/builtins/math/rad2deg.md index 545f01b13f..a09e09c7b9 100644 --- a/docs/internals/builtins/math/rad2deg.md +++ b/docs/internals/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg() — internals" description: "Compiler internals for rad2deg(): lowering path, type checks, and runtime helpers." sidebar: - order: 281 + order: 295 --- ## `rad2deg()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rad2deg.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/rand.md b/docs/internals/builtins/math/rand.md index 2148957917..55a80a8e7c 100644 --- a/docs/internals/builtins/math/rand.md +++ b/docs/internals/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand() — internals" description: "Compiler internals for rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 282 + order: 296 --- ## `rand()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/rand.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/random_int.md b/docs/internals/builtins/math/random_int.md index c86741daef..a94d645a8c 100644 --- a/docs/internals/builtins/math/random_int.md +++ b/docs/internals/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int() — internals" description: "Compiler internals for random_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 283 + order: 297 --- ## `random_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/random_int.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (2 declared effects)` +- **Effects**: `static (3 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/math/round.md b/docs/internals/builtins/math/round.md index 09dcf188c9..6704b71ed4 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round() — internals" description: "Compiler internals for round(): lowering path, type checks, and runtime helpers." sidebar: - order: 284 + order: 298 --- ## `round()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/round.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` @@ -39,12 +39,12 @@ sidebar: ## Signature summary ```php -function round(float $num, int $precision = 0): float +function round(float $num, int $precision = 0, int $mode = 1): float ``` ## What the type checker enforces -- **Arity**: takes 1–2 arguments (1 optional). +- **Arity**: takes 1–3 arguments (2 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/math/sin.md b/docs/internals/builtins/math/sin.md index 0d32e8761e..ebb81443ea 100644 --- a/docs/internals/builtins/math/sin.md +++ b/docs/internals/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin() — internals" description: "Compiler internals for sin(): lowering path, type checks, and runtime helpers." sidebar: - order: 285 + order: 299 --- ## `sin()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/sinh.md b/docs/internals/builtins/math/sinh.md index 3fccece5c0..736314a193 100644 --- a/docs/internals/builtins/math/sinh.md +++ b/docs/internals/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh() — internals" description: "Compiler internals for sinh(): lowering path, type checks, and runtime helpers." sidebar: - order: 286 + order: 300 --- ## `sinh()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sinh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/sqrt.md b/docs/internals/builtins/math/sqrt.md index d5fe1451e6..209affd958 100644 --- a/docs/internals/builtins/math/sqrt.md +++ b/docs/internals/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt() — internals" description: "Compiler internals for sqrt(): lowering path, type checks, and runtime helpers." sidebar: - order: 287 + order: 301 --- ## `sqrt()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/sqrt.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/tan.md b/docs/internals/builtins/math/tan.md index 333b5cf475..598378d049 100644 --- a/docs/internals/builtins/math/tan.md +++ b/docs/internals/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan() — internals" description: "Compiler internals for tan(): lowering path, type checks, and runtime helpers." sidebar: - order: 288 + order: 302 --- ## `tan()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tan.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/math/tanh.md b/docs/internals/builtins/math/tanh.md index 6734e0afbf..2a19fdc40d 100644 --- a/docs/internals/builtins/math/tanh.md +++ b/docs/internals/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh() — internals" description: "Compiler internals for tanh(): lowering path, type checks, and runtime helpers." sidebar: - order: 289 + order: 303 --- ## `tanh()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/math/tanh.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/buffer_new.md b/docs/internals/builtins/misc/buffer_new.md index 1ea3184f78..f68ed3bd6a 100644 --- a/docs/internals/builtins/misc/buffer_new.md +++ b/docs/internals/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new() — internals" description: "Compiler internals for buffer_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 290 + order: 304 --- ## `buffer_new()` — internals diff --git a/docs/internals/builtins/misc/constant.md b/docs/internals/builtins/misc/constant.md new file mode 100644 index 0000000000..34ee6f2ac7 --- /dev/null +++ b/docs/internals/builtins/misc/constant.md @@ -0,0 +1,54 @@ +--- +title: "constant() — internals" +description: "Compiler internals for constant(): lowering path, type checks, and runtime helpers." +sidebar: + order: 305 +--- + +## `constant()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/system/constant.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/constant.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `eir_primitive` strategy from the single-source builtin descriptor. +- Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`. + +## Semantic descriptor + +- **Target strategy**: `eir_primitive` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `non_heap` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: descriptor-emitted EIR primitives or graph; no opaque builtin call remains. + +## Signature summary + +```php +function constant(string $name): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/constant.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/constant.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `constant()`](../../../php/builtins/misc/constant.md) diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index f61bbd8f70..25a6aa2989 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define() — internals" description: "Compiler internals for define(): lowering path, type checks, and runtime helpers." sidebar: - order: 291 + order: 306 --- ## `define()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/define.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/define.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index 52f198cd62..8b4812f29c 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined() — internals" description: "Compiler internals for defined(): lowering path, type checks, and runtime helpers." sidebar: - order: 292 + order: 307 --- ## `defined()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index c046799ba5..a5aa9a54aa 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty() — internals" description: "Compiler internals for empty(): lowering path, type checks, and runtime helpers." sidebar: - order: 293 + order: 308 --- ## `empty()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/count_empty.rs`:88](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/count_empty.rs#L88) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins/count_empty.rs`:92](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/count_empty.rs#L92) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/misc/extension_loaded.md b/docs/internals/builtins/misc/extension_loaded.md index cd9c3bb13d..8ebe3da69e 100644 --- a/docs/internals/builtins/misc/extension_loaded.md +++ b/docs/internals/builtins/misc/extension_loaded.md @@ -2,7 +2,7 @@ title: "extension_loaded() — internals" description: "Compiler internals for extension_loaded(): lowering path, type checks, and runtime helpers." sidebar: - order: 294 + order: 309 --- ## `extension_loaded()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/extension_loaded.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/extension_loaded.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/get_loaded_extensions.md b/docs/internals/builtins/misc/get_loaded_extensions.md index 97ad2f8ae8..8c60abf410 100644 --- a/docs/internals/builtins/misc/get_loaded_extensions.md +++ b/docs/internals/builtins/misc/get_loaded_extensions.md @@ -2,7 +2,7 @@ title: "get_loaded_extensions() — internals" description: "Compiler internals for get_loaded_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 295 + order: 310 --- ## `get_loaded_extensions()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/get_loaded_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/get_loaded_extensions.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/header.md b/docs/internals/builtins/misc/header.md index dc0bcba995..8b0701b452 100644 --- a/docs/internals/builtins/misc/header.md +++ b/docs/internals/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header() — internals" description: "Compiler internals for header(): lowering path, type checks, and runtime helpers." sidebar: - order: 296 + order: 311 --- ## `header()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/header.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/header.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/http_response_code.md b/docs/internals/builtins/misc/http_response_code.md index 9e8465088a..ca3cc30a36 100644 --- a/docs/internals/builtins/misc/http_response_code.md +++ b/docs/internals/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code() — internals" description: "Compiler internals for http_response_code(): lowering path, type checks, and runtime helpers." sidebar: - order: 297 + order: 312 --- ## `http_response_code()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/http_response_code.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/isset.md b/docs/internals/builtins/misc/isset.md index 3239432d7a..98d69fc1b0 100644 --- a/docs/internals/builtins/misc/isset.md +++ b/docs/internals/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset() — internals" description: "Compiler internals for isset(): lowering path, type checks, and runtime helpers." sidebar: - order: 298 + order: 313 --- ## `isset()` — internals diff --git a/docs/internals/builtins/misc/php_uname.md b/docs/internals/builtins/misc/php_uname.md index b6d46f26f4..cbbdb0d3fe 100644 --- a/docs/internals/builtins/misc/php_uname.md +++ b/docs/internals/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname() — internals" description: "Compiler internals for php_uname(): lowering path, type checks, and runtime helpers." sidebar: - order: 299 + order: 314 --- ## `php_uname()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/php_uname.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index 5a58c5815f..e7e03c79f7 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion() — internals" description: "Compiler internals for phpversion(): lowering path, type checks, and runtime helpers." sidebar: - order: 300 + order: 315 --- ## `phpversion()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/print_r.md b/docs/internals/builtins/misc/print_r.md index dabbbf8544..c59e976e3e 100644 --- a/docs/internals/builtins/misc/print_r.md +++ b/docs/internals/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r() — internals" description: "Compiler internals for print_r(): lowering path, type checks, and runtime helpers." sidebar: - order: 301 + order: 316 --- ## `print_r()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/print_r.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/serialize.md b/docs/internals/builtins/misc/serialize.md index 88d7dcc3cd..5133ebbde6 100644 --- a/docs/internals/builtins/misc/serialize.md +++ b/docs/internals/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize() — internals" description: "Compiler internals for serialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 302 + order: 317 --- ## `serialize()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/serialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/serialize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/unserialize.md b/docs/internals/builtins/misc/unserialize.md index deab79620d..0049241177 100644 --- a/docs/internals/builtins/misc/unserialize.md +++ b/docs/internals/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize() — internals" description: "Compiler internals for unserialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 303 + order: 318 --- ## `unserialize()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/unserialize.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/unserialize.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/misc/unset.md b/docs/internals/builtins/misc/unset.md index 3e77bdfd30..f5feb8dfa0 100644 --- a/docs/internals/builtins/misc/unset.md +++ b/docs/internals/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset() — internals" description: "Compiler internals for unset(): lowering path, type checks, and runtime helpers." sidebar: - order: 304 + order: 319 --- ## `unset()` — internals @@ -10,13 +10,26 @@ 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/types.rs`:49](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L49) (`lower_unset_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:140](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L140) (`lower_unset_builtin`) - **Function symbol**: `lower_unset_builtin()` ### Lowering notes - Rejects `unset()` calls that were not converted into direct EIR unbind operations. +- Reaching this lowering means `crate::ir_lower::expr` could not turn the target +- into a slot clear, a hash/array removal, an `offsetUnset()` call, a `__unset()` +- call or a dynamic-property removal, so the message lists the shapes that do lower +- directly and then names the one shape users hit most. +- THE UNTYPED FIXED SLOT is that shape. `unset($obj->untypedProp)` on a property +- declared without a type (`public $foo = 1;`) truly REMOVES it in PHP: a later read +- warns `Undefined property` and answers `null`, and a later write recreates it. +- elephc gives each declared property a fixed, monomorphically typed slot, so a +- property the checker typed `Int` has no encoding for "removed and reading as null" +- — every candidate encoding answers `int(0)` or a raw marker word instead. A loud +- error beats a wrong value, so the shape is refused here. Untyped properties whose +- storage is a DYNAMIC hash (`stdClass`, undeclared names on +- `#[AllowDynamicProperties]` classes) are genuinely removable and lower fine. ## Semantic descriptor diff --git a/docs/internals/builtins/misc/var_dump.md b/docs/internals/builtins/misc/var_dump.md index d0f908f1c8..12bcaa5529 100644 --- a/docs/internals/builtins/misc/var_dump.md +++ b/docs/internals/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump() — internals" description: "Compiler internals for var_dump(): lowering path, type checks, and runtime helpers." sidebar: - order: 305 + order: 320 --- ## `var_dump()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/var_dump.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr.md b/docs/internals/builtins/pointer/ptr.md index 6f3270fc98..e6643f8fd1 100644 --- a/docs/internals/builtins/pointer/ptr.md +++ b/docs/internals/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr() — internals" description: "Compiler internals for ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 306 + order: 321 --- ## `ptr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_get.md b/docs/internals/builtins/pointer/ptr_get.md index 1888bd648c..b9ce6a60c6 100644 --- a/docs/internals/builtins/pointer/ptr_get.md +++ b/docs/internals/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get() — internals" description: "Compiler internals for ptr_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 307 + order: 322 --- ## `ptr_get()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_get.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_is_null.md b/docs/internals/builtins/pointer/ptr_is_null.md index e50b104764..cfbaea0f06 100644 --- a/docs/internals/builtins/pointer/ptr_is_null.md +++ b/docs/internals/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null() — internals" description: "Compiler internals for ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 308 + order: 323 --- ## `ptr_is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_null.md b/docs/internals/builtins/pointer/ptr_null.md index 4f358e53a9..85e0dc69d4 100644 --- a/docs/internals/builtins/pointer/ptr_null.md +++ b/docs/internals/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null() — internals" description: "Compiler internals for ptr_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 309 + order: 324 --- ## `ptr_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_offset.md b/docs/internals/builtins/pointer/ptr_offset.md index 4d287eece4..cbaa8f3963 100644 --- a/docs/internals/builtins/pointer/ptr_offset.md +++ b/docs/internals/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset() — internals" description: "Compiler internals for ptr_offset(): lowering path, type checks, and runtime helpers." sidebar: - order: 310 + order: 325 --- ## `ptr_offset()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_offset.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read16.md b/docs/internals/builtins/pointer/ptr_read16.md index da1edc0192..1017789ab2 100644 --- a/docs/internals/builtins/pointer/ptr_read16.md +++ b/docs/internals/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16() — internals" description: "Compiler internals for ptr_read16(): lowering path, type checks, and runtime helpers." sidebar: - order: 311 + order: 326 --- ## `ptr_read16()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read16.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read32.md b/docs/internals/builtins/pointer/ptr_read32.md index 7acbb14207..c1840f9387 100644 --- a/docs/internals/builtins/pointer/ptr_read32.md +++ b/docs/internals/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32() — internals" description: "Compiler internals for ptr_read32(): lowering path, type checks, and runtime helpers." sidebar: - order: 312 + order: 327 --- ## `ptr_read32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read8.md b/docs/internals/builtins/pointer/ptr_read8.md index 5289c2b8d9..6fbbbf081a 100644 --- a/docs/internals/builtins/pointer/ptr_read8.md +++ b/docs/internals/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8() — internals" description: "Compiler internals for ptr_read8(): lowering path, type checks, and runtime helpers." sidebar: - order: 313 + order: 328 --- ## `ptr_read8()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read8.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_read_string.md b/docs/internals/builtins/pointer/ptr_read_string.md index 882b1679e9..48c44e3a4e 100644 --- a/docs/internals/builtins/pointer/ptr_read_string.md +++ b/docs/internals/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string() — internals" description: "Compiler internals for ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 314 + order: 329 --- ## `ptr_read_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_read_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_set.md b/docs/internals/builtins/pointer/ptr_set.md index 2a891b4da8..9601aa8038 100644 --- a/docs/internals/builtins/pointer/ptr_set.md +++ b/docs/internals/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set() — internals" description: "Compiler internals for ptr_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 315 + order: 330 --- ## `ptr_set()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_set.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_sizeof.md b/docs/internals/builtins/pointer/ptr_sizeof.md index 98ede4ef02..4b3b518f1a 100644 --- a/docs/internals/builtins/pointer/ptr_sizeof.md +++ b/docs/internals/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof() — internals" description: "Compiler internals for ptr_sizeof(): lowering path, type checks, and runtime helpers." sidebar: - order: 316 + order: 331 --- ## `ptr_sizeof()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_sizeof.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write16.md b/docs/internals/builtins/pointer/ptr_write16.md index ee9286f041..dc4ff23dd4 100644 --- a/docs/internals/builtins/pointer/ptr_write16.md +++ b/docs/internals/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16() — internals" description: "Compiler internals for ptr_write16(): lowering path, type checks, and runtime helpers." sidebar: - order: 317 + order: 332 --- ## `ptr_write16()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write16.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write32.md b/docs/internals/builtins/pointer/ptr_write32.md index 9df31cb4d2..7c74b81fa3 100644 --- a/docs/internals/builtins/pointer/ptr_write32.md +++ b/docs/internals/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32() — internals" description: "Compiler internals for ptr_write32(): lowering path, type checks, and runtime helpers." sidebar: - order: 318 + order: 333 --- ## `ptr_write32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write8.md b/docs/internals/builtins/pointer/ptr_write8.md index 0e3b65d146..173f6a22da 100644 --- a/docs/internals/builtins/pointer/ptr_write8.md +++ b/docs/internals/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8() — internals" description: "Compiler internals for ptr_write8(): lowering path, type checks, and runtime helpers." sidebar: - order: 319 + order: 334 --- ## `ptr_write8()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write8.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/ptr_write_string.md b/docs/internals/builtins/pointer/ptr_write_string.md index 57100ad297..7ee9bd167a 100644 --- a/docs/internals/builtins/pointer/ptr_write_string.md +++ b/docs/internals/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string() — internals" description: "Compiler internals for ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 320 + order: 335 --- ## `ptr_write_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/ptr_write_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_free.md b/docs/internals/builtins/pointer/zval_free.md index cbba58a499..37dda366d0 100644 --- a/docs/internals/builtins/pointer/zval_free.md +++ b/docs/internals/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free() — internals" description: "Compiler internals for zval_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 321 + order: 336 --- ## `zval_free()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_free.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_free.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_pack.md b/docs/internals/builtins/pointer/zval_pack.md index 360ad50c01..1f34ba0532 100644 --- a/docs/internals/builtins/pointer/zval_pack.md +++ b/docs/internals/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack() — internals" description: "Compiler internals for zval_pack(): lowering path, type checks, and runtime helpers." sidebar: - order: 322 + order: 337 --- ## `zval_pack()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_pack.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_pack.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_type.md b/docs/internals/builtins/pointer/zval_type.md index 0a93594394..16eed3b220 100644 --- a/docs/internals/builtins/pointer/zval_type.md +++ b/docs/internals/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type() — internals" description: "Compiler internals for zval_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 323 + order: 338 --- ## `zval_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_type.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/pointer/zval_unpack.md b/docs/internals/builtins/pointer/zval_unpack.md index db9020fd04..1d506deca5 100644 --- a/docs/internals/builtins/pointer/zval_unpack.md +++ b/docs/internals/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack() — internals" description: "Compiler internals for zval_unpack(): lowering path, type checks, and runtime helpers." sidebar: - order: 324 + order: 339 --- ## `zval_unpack()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/pointers/zval_unpack.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/pointers/zval_unpack.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/die.md b/docs/internals/builtins/process/die.md index ff949dd437..86f47e381a 100644 --- a/docs/internals/builtins/process/die.md +++ b/docs/internals/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die() — internals" description: "Compiler internals for die(): lowering path, type checks, and runtime helpers." sidebar: - order: 325 + order: 340 --- ## `die()` — internals diff --git a/docs/internals/builtins/process/exec.md b/docs/internals/builtins/process/exec.md index e96a6a9a6d..a7406d63bd 100644 --- a/docs/internals/builtins/process/exec.md +++ b/docs/internals/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec() — internals" description: "Compiler internals for exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 326 + order: 341 --- ## `exec()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/exec.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/exit.md b/docs/internals/builtins/process/exit.md index bac7225865..f806740378 100644 --- a/docs/internals/builtins/process/exit.md +++ b/docs/internals/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit() — internals" description: "Compiler internals for exit(): lowering path, type checks, and runtime helpers." sidebar: - order: 327 + order: 342 --- ## `exit()` — internals diff --git a/docs/internals/builtins/process/passthru.md b/docs/internals/builtins/process/passthru.md index e855a93516..1770718a5c 100644 --- a/docs/internals/builtins/process/passthru.md +++ b/docs/internals/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru() — internals" description: "Compiler internals for passthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 328 + order: 343 --- ## `passthru()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/passthru.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/pclose.md b/docs/internals/builtins/process/pclose.md index 40a16e8956..3a40ba5366 100644 --- a/docs/internals/builtins/process/pclose.md +++ b/docs/internals/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose() — internals" description: "Compiler internals for pclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 329 + order: 344 --- ## `pclose()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pclose.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/popen.md b/docs/internals/builtins/process/popen.md index df3164ba4f..2688868bb6 100644 --- a/docs/internals/builtins/process/popen.md +++ b/docs/internals/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen() — internals" description: "Compiler internals for popen(): lowering path, type checks, and runtime helpers." sidebar: - order: 330 + order: 345 --- ## `popen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/popen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/readline.md b/docs/internals/builtins/process/readline.md index b05bf9f980..657019cdd0 100644 --- a/docs/internals/builtins/process/readline.md +++ b/docs/internals/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline() — internals" description: "Compiler internals for readline(): lowering path, type checks, and runtime helpers." sidebar: - order: 331 + order: 346 --- ## `readline()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/readline.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/shell_exec.md b/docs/internals/builtins/process/shell_exec.md index 2a438e48f9..bdcf5ac9f6 100644 --- a/docs/internals/builtins/process/shell_exec.md +++ b/docs/internals/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec() — internals" description: "Compiler internals for shell_exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 332 + order: 347 --- ## `shell_exec()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/shell_exec.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/sleep.md b/docs/internals/builtins/process/sleep.md index 4eeb8660b6..e764cad057 100644 --- a/docs/internals/builtins/process/sleep.md +++ b/docs/internals/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep() — internals" description: "Compiler internals for sleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 333 + order: 348 --- ## `sleep()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/sleep.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/system.md b/docs/internals/builtins/process/system.md index 979aa6e212..14359ada25 100644 --- a/docs/internals/builtins/process/system.md +++ b/docs/internals/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system() — internals" description: "Compiler internals for system(): lowering path, type checks, and runtime helpers." sidebar: - order: 334 + order: 349 --- ## `system()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/system.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/system.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/process/usleep.md b/docs/internals/builtins/process/usleep.md index a570a44137..efc777445e 100644 --- a/docs/internals/builtins/process/usleep.md +++ b/docs/internals/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep() — internals" description: "Compiler internals for usleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 335 + order: 350 --- ## `usleep()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/usleep.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/mb_ereg_match.md b/docs/internals/builtins/regex/mb_ereg_match.md index 9600064748..a572f4e117 100644 --- a/docs/internals/builtins/regex/mb_ereg_match.md +++ b/docs/internals/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match() — internals" description: "Compiler internals for mb_ereg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 336 + order: 351 --- ## `mb_ereg_match()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/mb_ereg_match.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_match.md b/docs/internals/builtins/regex/preg_match.md index 2b36614e83..dfcc5bf4b7 100644 --- a/docs/internals/builtins/regex/preg_match.md +++ b/docs/internals/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match() — internals" description: "Compiler internals for preg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 337 + order: 352 --- ## `preg_match()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_match_all.md b/docs/internals/builtins/regex/preg_match_all.md index dfcdacc234..de29624205 100644 --- a/docs/internals/builtins/regex/preg_match_all.md +++ b/docs/internals/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all() — internals" description: "Compiler internals for preg_match_all(): lowering path, type checks, and runtime helpers." sidebar: - order: 338 + order: 353 --- ## `preg_match_all()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_match_all.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_replace.md b/docs/internals/builtins/regex/preg_replace.md index da28341b2c..b6ded40cab 100644 --- a/docs/internals/builtins/regex/preg_replace.md +++ b/docs/internals/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace() — internals" description: "Compiler internals for preg_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 339 + order: 354 --- ## `preg_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_replace_callback.md b/docs/internals/builtins/regex/preg_replace_callback.md index 4aa525a0e2..e3821c6d0d 100644 --- a/docs/internals/builtins/regex/preg_replace_callback.md +++ b/docs/internals/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback() — internals" description: "Compiler internals for preg_replace_callback(): lowering path, type checks, and runtime helpers." sidebar: - order: 340 + order: 355 --- ## `preg_replace_callback()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/preg_replace_callback.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/regex/preg_split.md b/docs/internals/builtins/regex/preg_split.md index d28cbc4423..99467dc82c 100644 --- a/docs/internals/builtins/regex/preg_split.md +++ b/docs/internals/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split() — internals" description: "Compiler internals for preg_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 341 + order: 356 --- ## `preg_split()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/preg_split.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_apply.md b/docs/internals/builtins/spl/iterator_apply.md index 49543aa486..70962c1afb 100644 --- a/docs/internals/builtins/spl/iterator_apply.md +++ b/docs/internals/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply() — internals" description: "Compiler internals for iterator_apply(): lowering path, type checks, and runtime helpers." sidebar: - order: 342 + order: 357 --- ## `iterator_apply()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_apply.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_count.md b/docs/internals/builtins/spl/iterator_count.md index 6dd7bc64d0..f5d5ba9c55 100644 --- a/docs/internals/builtins/spl/iterator_count.md +++ b/docs/internals/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count() — internals" description: "Compiler internals for iterator_count(): lowering path, type checks, and runtime helpers." sidebar: - order: 343 + order: 358 --- ## `iterator_count()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_count.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/iterator_to_array.md b/docs/internals/builtins/spl/iterator_to_array.md index 641cfa817e..cc2a09b06c 100644 --- a/docs/internals/builtins/spl/iterator_to_array.md +++ b/docs/internals/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array() — internals" description: "Compiler internals for iterator_to_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 344 + order: 359 --- ## `iterator_to_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/iterator_to_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload.md b/docs/internals/builtins/spl/spl_autoload.md index c841e025fe..23f5bab2af 100644 --- a/docs/internals/builtins/spl/spl_autoload.md +++ b/docs/internals/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload() — internals" description: "Compiler internals for spl_autoload(): lowering path, type checks, and runtime helpers." sidebar: - order: 345 + order: 360 --- ## `spl_autoload()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_call.md b/docs/internals/builtins/spl/spl_autoload_call.md index be7ac6e5e9..bf82164378 100644 --- a/docs/internals/builtins/spl/spl_autoload_call.md +++ b/docs/internals/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call() — internals" description: "Compiler internals for spl_autoload_call(): lowering path, type checks, and runtime helpers." sidebar: - order: 346 + order: 361 --- ## `spl_autoload_call()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_call.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_extensions.md b/docs/internals/builtins/spl/spl_autoload_extensions.md index 55a8874d1e..4934c1f90a 100644 --- a/docs/internals/builtins/spl/spl_autoload_extensions.md +++ b/docs/internals/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions() — internals" description: "Compiler internals for spl_autoload_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 347 + order: 362 --- ## `spl_autoload_extensions()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_extensions.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_functions.md b/docs/internals/builtins/spl/spl_autoload_functions.md index 790548c30e..00f237cb5e 100644 --- a/docs/internals/builtins/spl/spl_autoload_functions.md +++ b/docs/internals/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions() — internals" description: "Compiler internals for spl_autoload_functions(): lowering path, type checks, and runtime helpers." sidebar: - order: 348 + order: 363 --- ## `spl_autoload_functions()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_functions.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_register.md b/docs/internals/builtins/spl/spl_autoload_register.md index 71106a199c..08112f531b 100644 --- a/docs/internals/builtins/spl/spl_autoload_register.md +++ b/docs/internals/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register() — internals" description: "Compiler internals for spl_autoload_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 349 + order: 364 --- ## `spl_autoload_register()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_register.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_autoload_unregister.md b/docs/internals/builtins/spl/spl_autoload_unregister.md index 33975f78e7..d279da5f6b 100644 --- a/docs/internals/builtins/spl/spl_autoload_unregister.md +++ b/docs/internals/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister() — internals" description: "Compiler internals for spl_autoload_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 350 + order: 365 --- ## `spl_autoload_unregister()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_autoload_unregister.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_classes.md b/docs/internals/builtins/spl/spl_classes.md index 1c841cf4e4..8a567f1c9b 100644 --- a/docs/internals/builtins/spl/spl_classes.md +++ b/docs/internals/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes() — internals" description: "Compiler internals for spl_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 351 + order: 366 --- ## `spl_classes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_classes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_object_hash.md b/docs/internals/builtins/spl/spl_object_hash.md index bdc677517c..17a1f46f50 100644 --- a/docs/internals/builtins/spl/spl_object_hash.md +++ b/docs/internals/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash() — internals" description: "Compiler internals for spl_object_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 352 + order: 367 --- ## `spl_object_hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/spl/spl_object_id.md b/docs/internals/builtins/spl/spl_object_id.md index 0b9c483e0c..3cf741673e 100644 --- a/docs/internals/builtins/spl/spl_object_id.md +++ b/docs/internals/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id() — internals" description: "Compiler internals for spl_object_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 353 + order: 368 --- ## `spl_object_id()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/spl/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/spl/spl_object_id.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/fsockopen.md b/docs/internals/builtins/streams/fsockopen.md index 170826d5d7..bfbcbaa3aa 100644 --- a/docs/internals/builtins/streams/fsockopen.md +++ b/docs/internals/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen() — internals" description: "Compiler internals for fsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 354 + order: 369 --- ## `fsockopen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/fsockopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/pfsockopen.md b/docs/internals/builtins/streams/pfsockopen.md index 1aaf4d3efb..90310c7aa1 100644 --- a/docs/internals/builtins/streams/pfsockopen.md +++ b/docs/internals/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen() — internals" description: "Compiler internals for pfsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 355 + order: 370 --- ## `pfsockopen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/pfsockopen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_bucket_append.md b/docs/internals/builtins/streams/stream_bucket_append.md index b59cbb74a2..5d6a7c7ab7 100644 --- a/docs/internals/builtins/streams/stream_bucket_append.md +++ b/docs/internals/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append() — internals" description: "Compiler internals for stream_bucket_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 356 + order: 371 --- ## `stream_bucket_append()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_append.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_bucket_prepend.md b/docs/internals/builtins/streams/stream_bucket_prepend.md index 0c569114ad..a7eeeba6fb 100644 --- a/docs/internals/builtins/streams/stream_bucket_prepend.md +++ b/docs/internals/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend() — internals" description: "Compiler internals for stream_bucket_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 357 + order: 372 --- ## `stream_bucket_prepend()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_bucket_prepend.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_filter_append.md b/docs/internals/builtins/streams/stream_filter_append.md index 01e129dd20..a7c1ce1297 100644 --- a/docs/internals/builtins/streams/stream_filter_append.md +++ b/docs/internals/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append() — internals" description: "Compiler internals for stream_filter_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 358 + order: 373 --- ## `stream_filter_append()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_append.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/streams/stream_filter_prepend.md b/docs/internals/builtins/streams/stream_filter_prepend.md index 876d793ed4..df6df8f904 100644 --- a/docs/internals/builtins/streams/stream_filter_prepend.md +++ b/docs/internals/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend() — internals" description: "Compiler internals for stream_filter_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 359 + order: 374 --- ## `stream_filter_prepend()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/io/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/io/stream_filter_prepend.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/addslashes.md b/docs/internals/builtins/string/addslashes.md index d90b94e5d5..9b37354411 100644 --- a/docs/internals/builtins/string/addslashes.md +++ b/docs/internals/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes() — internals" description: "Compiler internals for addslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 360 + order: 375 --- ## `addslashes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/addslashes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/base64_decode.md b/docs/internals/builtins/string/base64_decode.md index a1ce7ca2be..ce3960be21 100644 --- a/docs/internals/builtins/string/base64_decode.md +++ b/docs/internals/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode() — internals" description: "Compiler internals for base64_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 361 + order: 376 --- ## `base64_decode()` — internals @@ -10,41 +10,41 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` ### Lowering notes - Uses the `runtime_call` strategy from the single-source builtin descriptor. -- Emits the typed EIR target `runtime.string.base64_decode` through `BuiltinLoweringContext`. +- Emits the typed EIR target `runtime.base64_decode` through `BuiltinLoweringContext`. - The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. ## Semantic descriptor - **Target strategy**: `runtime_call` -- **Validation**: `signature` -- **Result type source**: `declared` +- **Validation**: `checker_hook` +- **Result type source**: `checked` - **Result ownership**: `fresh` - **Effects**: `static (0 declared effects)` - **Requirements**: `static (0 requirements)` -- **Callable policy**: `dynamic` +- **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` ## EIR and runtime boundary -- **Typed EIR target**: `runtime.string.base64_decode` +- **Typed EIR target**: `runtime.base64_decode` - **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. ## Signature summary ```php -function base64_decode(string $string): string +function base64_decode(string $string, bool $strict = false): mixed ``` ## What the type checker enforces -- **Arity**: takes exactly 1 argument. +- **Arity**: takes 1–2 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/string/base64_encode.md b/docs/internals/builtins/string/base64_encode.md index 21c33538d0..49257cca84 100644 --- a/docs/internals/builtins/string/base64_encode.md +++ b/docs/internals/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode() — internals" description: "Compiler internals for base64_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 362 + order: 377 --- ## `base64_encode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/base64_encode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/bin2hex.md b/docs/internals/builtins/string/bin2hex.md index 64156fb181..5a6e047daa 100644 --- a/docs/internals/builtins/string/bin2hex.md +++ b/docs/internals/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex() — internals" description: "Compiler internals for bin2hex(): lowering path, type checks, and runtime helpers." sidebar: - order: 363 + order: 378 --- ## `bin2hex()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/bin2hex.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/chop.md b/docs/internals/builtins/string/chop.md index 27ad9c57ff..ff3b172894 100644 --- a/docs/internals/builtins/string/chop.md +++ b/docs/internals/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop() — internals" description: "Compiler internals for chop(): lowering path, type checks, and runtime helpers." sidebar: - order: 364 + order: 379 --- ## `chop()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index bd6b407fbc..0f6135ad3b 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr() — internals" description: "Compiler internals for chr(): lowering path, type checks, and runtime helpers." sidebar: - order: 365 + order: 380 --- ## `chr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/chunk_split.md b/docs/internals/builtins/string/chunk_split.md new file mode 100644 index 0000000000..45d0a2fd56 --- /dev/null +++ b/docs/internals/builtins/string/chunk_split.md @@ -0,0 +1,56 @@ +--- +title: "chunk_split() — internals" +description: "Compiler internals for chunk_split(): lowering path, type checks, and runtime helpers." +sidebar: + order: 381 +--- + +## `chunk_split()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/chunk_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/chunk_split.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.chunk_split` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.chunk_split` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function chunk_split(string $string, int $length = 76, string $separator = '\r\n'): string +``` + +## What the type checker enforces + +- **Arity**: takes 1–3 arguments (2 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `chunk_split()`](../../../php/builtins/string/chunk_split.md) diff --git a/docs/internals/builtins/string/count_chars.md b/docs/internals/builtins/string/count_chars.md new file mode 100644 index 0000000000..73c71b5308 --- /dev/null +++ b/docs/internals/builtins/string/count_chars.md @@ -0,0 +1,56 @@ +--- +title: "count_chars() — internals" +description: "Compiler internals for count_chars(): lowering path, type checks, and runtime helpers." +sidebar: + order: 382 +--- + +## `count_chars()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/count_chars.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/count_chars.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.count_chars` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.count_chars` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function count_chars(string $string, int $mode = 0): array|string +``` + +## What the type checker enforces + +- **Arity**: takes 1–2 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `count_chars()`](../../../php/builtins/string/count_chars.md) diff --git a/docs/internals/builtins/string/crc32.md b/docs/internals/builtins/string/crc32.md index 307acc868c..2dc8399326 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32() — internals" description: "Compiler internals for crc32(): lowering path, type checks, and runtime helpers." sidebar: - order: 366 + order: 383 --- ## `crc32()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/crc32.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/explode.md b/docs/internals/builtins/string/explode.md index 04f12fa36a..2f4917ee97 100644 --- a/docs/internals/builtins/string/explode.md +++ b/docs/internals/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode() — internals" description: "Compiler internals for explode(): lowering path, type checks, and runtime helpers." sidebar: - order: 367 + order: 384 --- ## `explode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/explode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/grapheme_strrev.md b/docs/internals/builtins/string/grapheme_strrev.md index 45eed21056..a47e4b8483 100644 --- a/docs/internals/builtins/string/grapheme_strrev.md +++ b/docs/internals/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev() — internals" description: "Compiler internals for grapheme_strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 368 + order: 385 --- ## `grapheme_strrev()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/grapheme_strrev.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzcompress.md b/docs/internals/builtins/string/gzcompress.md index 14f186e03c..24e62fd3b9 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress() — internals" description: "Compiler internals for gzcompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 369 + order: 386 --- ## `gzcompress()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzcompress.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzdeflate.md b/docs/internals/builtins/string/gzdeflate.md index 7f01fe46df..c22c7d7060 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate() — internals" description: "Compiler internals for gzdeflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 370 + order: 387 --- ## `gzdeflate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzdeflate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzinflate.md b/docs/internals/builtins/string/gzinflate.md index c891851bc9..f888e6e462 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate() — internals" description: "Compiler internals for gzinflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 371 + order: 388 --- ## `gzinflate()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzinflate.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/gzuncompress.md b/docs/internals/builtins/string/gzuncompress.md index 7e1b5a694e..8e2bd54fa6 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress() — internals" description: "Compiler internals for gzuncompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 372 + order: 389 --- ## `gzuncompress()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/gzuncompress.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash.md b/docs/internals/builtins/string/hash.md index c81df98044..35163478b6 100644 --- a/docs/internals/builtins/string/hash.md +++ b/docs/internals/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash() — internals" description: "Compiler internals for hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 373 + order: 390 --- ## `hash()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_algos.md b/docs/internals/builtins/string/hash_algos.md index e6dae1428b..f7e5f71411 100644 --- a/docs/internals/builtins/string/hash_algos.md +++ b/docs/internals/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos() — internals" description: "Compiler internals for hash_algos(): lowering path, type checks, and runtime helpers." sidebar: - order: 374 + order: 391 --- ## `hash_algos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_algos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_copy.md b/docs/internals/builtins/string/hash_copy.md index af36b04305..3ca94d179f 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy() — internals" description: "Compiler internals for hash_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 392 --- ## `hash_copy()` — internals diff --git a/docs/internals/builtins/string/hash_equals.md b/docs/internals/builtins/string/hash_equals.md index 57db497f38..b64b6bfce9 100644 --- a/docs/internals/builtins/string/hash_equals.md +++ b/docs/internals/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals() — internals" description: "Compiler internals for hash_equals(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 393 --- ## `hash_equals()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_equals.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_final.md b/docs/internals/builtins/string/hash_final.md index 2a2f2ebf8c..bd7bab15f9 100644 --- a/docs/internals/builtins/string/hash_final.md +++ b/docs/internals/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final() — internals" description: "Compiler internals for hash_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 394 --- ## `hash_final()` — internals diff --git a/docs/internals/builtins/string/hash_hmac.md b/docs/internals/builtins/string/hash_hmac.md index 659a56b165..8db61c5010 100644 --- a/docs/internals/builtins/string/hash_hmac.md +++ b/docs/internals/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac() — internals" description: "Compiler internals for hash_hmac(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 395 --- ## `hash_hmac()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hash_hmac.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/hash_init.md b/docs/internals/builtins/string/hash_init.md index 7223246d8e..cdb41dad80 100644 --- a/docs/internals/builtins/string/hash_init.md +++ b/docs/internals/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init() — internals" description: "Compiler internals for hash_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 396 --- ## `hash_init()` — internals diff --git a/docs/internals/builtins/string/hash_update.md b/docs/internals/builtins/string/hash_update.md index 3ac60f209c..f4db80858e 100644 --- a/docs/internals/builtins/string/hash_update.md +++ b/docs/internals/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update() — internals" description: "Compiler internals for hash_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 397 --- ## `hash_update()` — internals diff --git a/docs/internals/builtins/string/hex2bin.md b/docs/internals/builtins/string/hex2bin.md index c38d52a5a2..2b369d2de7 100644 --- a/docs/internals/builtins/string/hex2bin.md +++ b/docs/internals/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin() — internals" description: "Compiler internals for hex2bin(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 398 --- ## `hex2bin()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/hex2bin.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/html_entity_decode.md b/docs/internals/builtins/string/html_entity_decode.md index 80b768d932..cce33dc952 100644 --- a/docs/internals/builtins/string/html_entity_decode.md +++ b/docs/internals/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode() — internals" description: "Compiler internals for html_entity_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 399 --- ## `html_entity_decode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/html_entity_decode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/htmlentities.md b/docs/internals/builtins/string/htmlentities.md index 5b872c2490..d458d87dfe 100644 --- a/docs/internals/builtins/string/htmlentities.md +++ b/docs/internals/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities() — internals" description: "Compiler internals for htmlentities(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 400 --- ## `htmlentities()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlentities.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/htmlspecialchars.md b/docs/internals/builtins/string/htmlspecialchars.md index 0ce4e8b791..445b7495f4 100644 --- a/docs/internals/builtins/string/htmlspecialchars.md +++ b/docs/internals/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars() — internals" description: "Compiler internals for htmlspecialchars(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 401 --- ## `htmlspecialchars()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/htmlspecialchars.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/implode.md b/docs/internals/builtins/string/implode.md index 31ff513674..ce0ba088f2 100644 --- a/docs/internals/builtins/string/implode.md +++ b/docs/internals/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode() — internals" description: "Compiler internals for implode(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 402 --- ## `implode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/implode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -23,8 +23,8 @@ sidebar: ## Semantic descriptor - **Target strategy**: `runtime_call` -- **Validation**: `checker_hook` -- **Result type source**: `checked` +- **Validation**: `signature` +- **Result type source**: `declared` - **Result ownership**: `independent` - **Effects**: `static (0 declared effects)` - **Requirements**: `static (0 requirements)` diff --git a/docs/internals/builtins/string/inet_ntop.md b/docs/internals/builtins/string/inet_ntop.md index 8c21a2e60b..39655aa9e9 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop() — internals" description: "Compiler internals for inet_ntop(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 403 --- ## `inet_ntop()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_ntop.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/inet_pton.md b/docs/internals/builtins/string/inet_pton.md index 4d17d5c46a..235912803f 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton() — internals" description: "Compiler internals for inet_pton(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 404 --- ## `inet_pton()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/inet_pton.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ip2long.md b/docs/internals/builtins/string/ip2long.md index c95058c6bc..2fd828be9f 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long() — internals" description: "Compiler internals for ip2long(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 405 --- ## `ip2long()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ip2long.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/join.md b/docs/internals/builtins/string/join.md new file mode 100644 index 0000000000..6b0f84f625 --- /dev/null +++ b/docs/internals/builtins/string/join.md @@ -0,0 +1,55 @@ +--- +title: "join() — internals" +description: "Compiler internals for join(): lowering path, type checks, and runtime helpers." +sidebar: + order: 406 +--- + +## `join()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/join.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/join.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.implode` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `independent` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.implode` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function join(mixed $separator, mixed $array = null): string +``` + +## What the type checker enforces + +- **Arity**: takes 1–2 arguments (1 optional). + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- [User reference for `join()`](../../../php/builtins/string/join.md) diff --git a/docs/internals/builtins/string/lcfirst.md b/docs/internals/builtins/string/lcfirst.md index 054730d81a..218dede184 100644 --- a/docs/internals/builtins/string/lcfirst.md +++ b/docs/internals/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst() — internals" description: "Compiler internals for lcfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 407 --- ## `lcfirst()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/lcfirst.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/long2ip.md b/docs/internals/builtins/string/long2ip.md index da3f0528c6..28131ab63b 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip() — internals" description: "Compiler internals for long2ip(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 408 --- ## `long2ip()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/long2ip.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ltrim.md b/docs/internals/builtins/string/ltrim.md index 88c0aad21b..fa8d889e6f 100644 --- a/docs/internals/builtins/string/ltrim.md +++ b/docs/internals/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim() — internals" description: "Compiler internals for ltrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 409 --- ## `ltrim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ltrim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/mb_strlen.md b/docs/internals/builtins/string/mb_strlen.md index 004b19e2d5..b026f1085d 100644 --- a/docs/internals/builtins/string/mb_strlen.md +++ b/docs/internals/builtins/string/mb_strlen.md @@ -2,7 +2,7 @@ title: "mb_strlen() — internals" description: "Compiler internals for mb_strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 410 --- ## `mb_strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/mb_strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/mb_strlen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/md5.md b/docs/internals/builtins/string/md5.md index b8cd9f8053..66d67a80f5 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 411 --- ## `md5()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/md5.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/nl2br.md b/docs/internals/builtins/string/nl2br.md index 1365d25c16..6daf5acb8b 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 412 --- ## `nl2br()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/nl2br.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index 09b629761c..785ca16cc6 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 413 --- ## `number_format()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/number_format.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index efef82e763..8eb2c69ad9 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 414 --- ## `ord()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ord.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/parse_url.md b/docs/internals/builtins/string/parse_url.md index a12efedfda..8b891fa5d8 100644 --- a/docs/internals/builtins/string/parse_url.md +++ b/docs/internals/builtins/string/parse_url.md @@ -2,7 +2,7 @@ title: "parse_url() — internals" description: "Compiler internals for parse_url(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 415 --- ## `parse_url()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/parse_url.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/parse_url.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/printf.md b/docs/internals/builtins/string/printf.md index 5a277e58c4..94e01bdd8a 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 416 --- ## `printf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/printf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/quoted_printable_encode.md b/docs/internals/builtins/string/quoted_printable_encode.md new file mode 100644 index 0000000000..dfd8b31c1b --- /dev/null +++ b/docs/internals/builtins/string/quoted_printable_encode.md @@ -0,0 +1,56 @@ +--- +title: "quoted_printable_encode() — internals" +description: "Compiler internals for quoted_printable_encode(): lowering path, type checks, and runtime helpers." +sidebar: + order: 417 +--- + +## `quoted_printable_encode()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/quoted_printable_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/quoted_printable_encode.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.string.quoted_printable_encode` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `fresh` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `dynamic` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.string.quoted_printable_encode` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function quoted_printable_encode(string $string): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `quoted_printable_encode()`](../../../php/builtins/string/quoted_printable_encode.md) diff --git a/docs/internals/builtins/string/quotemeta.md b/docs/internals/builtins/string/quotemeta.md new file mode 100644 index 0000000000..def6a9dfb8 --- /dev/null +++ b/docs/internals/builtins/string/quotemeta.md @@ -0,0 +1,56 @@ +--- +title: "quotemeta() — internals" +description: "Compiler internals for quotemeta(): lowering path, type checks, and runtime helpers." +sidebar: + order: 418 +--- + +## `quotemeta()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/quotemeta.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/quotemeta.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.string.quote_meta` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `fresh` +- **Effects**: `static (0 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `dynamic` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.string.quote_meta` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function quotemeta(string $string): string +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `quotemeta()`](../../../php/builtins/string/quotemeta.md) diff --git a/docs/internals/builtins/string/rawurldecode.md b/docs/internals/builtins/string/rawurldecode.md index 43686c52ca..5bc06e4836 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 419 --- ## `rawurldecode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurldecode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/rawurlencode.md b/docs/internals/builtins/string/rawurlencode.md index 8a3ab9f789..93725326c3 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 420 --- ## `rawurlencode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rawurlencode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/rtrim.md b/docs/internals/builtins/string/rtrim.md index e23ea8c117..2fdd2809c5 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 421 --- ## `rtrim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/rtrim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sha1.md b/docs/internals/builtins/string/sha1.md index ebb72dd59c..7cacf42066 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 422 --- ## `sha1()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sha1.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sprintf.md b/docs/internals/builtins/string/sprintf.md index e0beb36005..f919f8da37 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 423 --- ## `sprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/sscanf.md b/docs/internals/builtins/string/sscanf.md index 0736ceda74..c1731f44fd 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 424 --- ## `sscanf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/sscanf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_contains.md b/docs/internals/builtins/string/str_contains.md index 7aee5253da..6c2e2e0c08 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 425 --- ## `str_contains()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_contains.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_ends_with.md b/docs/internals/builtins/string/str_ends_with.md index 7f0965744d..5a2c85089e 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 426 --- ## `str_ends_with()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ends_with.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index e2f15c24a5..2c1dfd5ba7 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 427 --- ## `str_ireplace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_ireplace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index 56754e7280..6815b187b9 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 428 --- ## `str_pad()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_pad.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index 3091ee926c..0eefd5d26c 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 429 --- ## `str_repeat()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_repeat.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 0bd91bbb78..d13c3a87b5 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 430 --- ## `str_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_split.md b/docs/internals/builtins/string/str_split.md index 489f88a9ab..9f3ffc73e4 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 431 --- ## `str_split()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_split.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/str_starts_with.md b/docs/internals/builtins/string/str_starts_with.md index 1f80b3c1de..25fbbc8946 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 432 --- ## `str_starts_with()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_starts_with.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/str_word_count.md b/docs/internals/builtins/string/str_word_count.md new file mode 100644 index 0000000000..952ae652c1 --- /dev/null +++ b/docs/internals/builtins/string/str_word_count.md @@ -0,0 +1,56 @@ +--- +title: "str_word_count() — internals" +description: "Compiler internals for str_word_count(): lowering path, type checks, and runtime helpers." +sidebar: + order: 433 +--- + +## `str_word_count()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/str_word_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/str_word_count.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.str_word_count` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.str_word_count` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function str_word_count(string $string, int $format = 0, string $characters = null): array|int +``` + +## What the type checker enforces + +- **Arity**: takes 1–3 arguments (2 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `str_word_count()`](../../../php/builtins/string/str_word_count.md) diff --git a/docs/internals/builtins/string/strcasecmp.md b/docs/internals/builtins/string/strcasecmp.md index 8ac83f8849..ea80a6b9dc 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 434 --- ## `strcasecmp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcasecmp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strcmp.md b/docs/internals/builtins/string/strcmp.md index e6b01a9b04..74c2861a41 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 435 --- ## `strcmp()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strcmp.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/stripos.md b/docs/internals/builtins/string/stripos.md new file mode 100644 index 0000000000..d18c6a4986 --- /dev/null +++ b/docs/internals/builtins/string/stripos.md @@ -0,0 +1,56 @@ +--- +title: "stripos() — internals" +description: "Compiler internals for stripos(): lowering path, type checks, and runtime helpers." +sidebar: + order: 436 +--- + +## `stripos()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/stripos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/stripos.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.stripos` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.stripos` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function stripos(string $haystack, string $needle, int $offset = 0): mixed +``` + +## What the type checker enforces + +- **Arity**: takes 2–3 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stripos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `stripos()`](../../../php/builtins/string/stripos.md) diff --git a/docs/internals/builtins/string/stripslashes.md b/docs/internals/builtins/string/stripslashes.md index ad16e5b75b..7f617331c9 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 437 --- ## `stripslashes()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/stripslashes.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 26c8918073..f9c42bb4c4 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 416 + order: 438 --- ## `strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strncasecmp.md b/docs/internals/builtins/string/strncasecmp.md new file mode 100644 index 0000000000..52d1f084f1 --- /dev/null +++ b/docs/internals/builtins/string/strncasecmp.md @@ -0,0 +1,55 @@ +--- +title: "strncasecmp() — internals" +description: "Compiler internals for strncasecmp(): lowering path, type checks, and runtime helpers." +sidebar: + order: 439 +--- + +## `strncasecmp()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/strncasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strncasecmp.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.strncasecmp` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.strncasecmp` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function strncasecmp(string $string1, string $string2, int $length): int +``` + +## What the type checker enforces + +- **Arity**: takes exactly 3 arguments. + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- [User reference for `strncasecmp()`](../../../php/builtins/string/strncasecmp.md) diff --git a/docs/internals/builtins/string/strncmp.md b/docs/internals/builtins/string/strncmp.md new file mode 100644 index 0000000000..e89e834758 --- /dev/null +++ b/docs/internals/builtins/string/strncmp.md @@ -0,0 +1,55 @@ +--- +title: "strncmp() — internals" +description: "Compiler internals for strncmp(): lowering path, type checks, and runtime helpers." +sidebar: + order: 440 +--- + +## `strncmp()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/strncmp.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strncmp.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.strncmp` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.strncmp` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function strncmp(string $string1, string $string2, int $length): int +``` + +## What the type checker enforces + +- **Arity**: takes exactly 3 arguments. + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- [User reference for `strncmp()`](../../../php/builtins/string/strncmp.md) diff --git a/docs/internals/builtins/string/strpos.md b/docs/internals/builtins/string/strpos.md index 24b10405a8..445092c993 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 417 + order: 441 --- ## `strpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strpos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/strrev.md b/docs/internals/builtins/string/strrev.md index bb466da91b..dcc2d2cc82 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 442 --- ## `strrev()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrev.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strripos.md b/docs/internals/builtins/string/strripos.md new file mode 100644 index 0000000000..6d0e8f3a84 --- /dev/null +++ b/docs/internals/builtins/string/strripos.md @@ -0,0 +1,56 @@ +--- +title: "strripos() — internals" +description: "Compiler internals for strripos(): lowering path, type checks, and runtime helpers." +sidebar: + order: 443 +--- + +## `strripos()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/strripos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strripos.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.strripos` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.strripos` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function strripos(string $haystack, string $needle, int $offset = 0): mixed +``` + +## What the type checker enforces + +- **Arity**: takes 2–3 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strripos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strripos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `strripos()`](../../../php/builtins/string/strripos.md) diff --git a/docs/internals/builtins/string/strrpos.md b/docs/internals/builtins/string/strrpos.md index 8fb4e91d7d..ec017c6099 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 444 --- ## `strrpos()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strrpos.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `checker_hook` - **Result type source**: `checked` - **Result ownership**: `fresh` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index 1cd19b3f50..7cf7f494da 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 445 --- ## `strstr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strstr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strtolower.md b/docs/internals/builtins/string/strtolower.md index ac344c5b59..54df765592 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 446 --- ## `strtolower()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtolower.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strtoupper.md b/docs/internals/builtins/string/strtoupper.md index d036835132..572b695248 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 447 --- ## `strtoupper()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtoupper.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/strtr.md b/docs/internals/builtins/string/strtr.md new file mode 100644 index 0000000000..d121e50612 --- /dev/null +++ b/docs/internals/builtins/string/strtr.md @@ -0,0 +1,56 @@ +--- +title: "strtr() — internals" +description: "Compiler internals for strtr(): lowering path, type checks, and runtime helpers." +sidebar: + order: 448 +--- + +## `strtr()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/strtr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strtr.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.strtr` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `checker_hook` +- **Result type source**: `checked` +- **Result ownership**: `fresh` +- **Effects**: `static (2 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.strtr` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function strtr(string $string, array|string $from, string $to = null): string +``` + +## What the type checker enforces + +- **Arity**: takes 2–3 arguments (1 optional). + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strtr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `strtr()`](../../../php/builtins/string/strtr.md) diff --git a/docs/internals/builtins/string/substr.md b/docs/internals/builtins/string/substr.md index ab7c416467..a3e5b59822 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 449 --- ## `substr()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/substr_count.md b/docs/internals/builtins/string/substr_count.md new file mode 100644 index 0000000000..0535959207 --- /dev/null +++ b/docs/internals/builtins/string/substr_count.md @@ -0,0 +1,55 @@ +--- +title: "substr_count() — internals" +description: "Compiler internals for substr_count(): lowering path, type checks, and runtime helpers." +sidebar: + order: 450 +--- + +## `substr_count()` — internals + +## Where it lives + +- **Signature**: [`src/builtins/string/substr_count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_count.rs) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) +- **Function symbol**: `lower_registry_call()` + + +### Lowering notes + +- Uses the `runtime_call` strategy from the single-source builtin descriptor. +- Emits the typed EIR target `runtime.substr_count` through `BuiltinLoweringContext`. +- The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch. + +## Semantic descriptor + +- **Target strategy**: `runtime_call` +- **Validation**: `signature` +- **Result type source**: `declared` +- **Result ownership**: `may_alias_arguments` +- **Effects**: `static (1 declared effects)` +- **Requirements**: `static (0 requirements)` +- **Callable policy**: `static_only` +- **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` + +## EIR and runtime boundary + +- **Typed EIR target**: `runtime.substr_count` +- **Backend boundary**: `src/codegen/lower_inst/runtime_calls.rs` resolves the typed target without PHP-name dispatch. + +## Signature summary + +```php +function substr_count(string $haystack, string $needle, int $offset = 0, mixed $length = null): int +``` + +## What the type checker enforces + +- **Arity**: takes 2–4 arguments (2 optional). + +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + +## Cross-references + +- [User reference for `substr_count()`](../../../php/builtins/string/substr_count.md) diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index 37242f6f38..50b358ffe8 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 451 --- ## `substr_replace()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/substr_replace.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/trim.md b/docs/internals/builtins/string/trim.md index da4e3ccbde..19bb3942ae 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 452 --- ## `trim()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/trim.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ucfirst.md b/docs/internals/builtins/string/ucfirst.md index e3a2b84802..0e7cd3aaac 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 453 --- ## `ucfirst()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucfirst.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/ucwords.md b/docs/internals/builtins/string/ucwords.md index f3df88d485..211ba76a2c 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 454 --- ## `ucwords()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ucwords.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/urldecode.md b/docs/internals/builtins/string/urldecode.md index 8427fb0ebf..e194ec19f2 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 455 --- ## `urldecode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urldecode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/urlencode.md b/docs/internals/builtins/string/urlencode.md index 607d26b61c..3d1e7f77de 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 456 --- ## `urlencode()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/urlencode.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/vprintf.md b/docs/internals/builtins/string/vprintf.md index b118a1a563..47f9f525ff 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 457 --- ## `vprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/vsprintf.md b/docs/internals/builtins/string/vsprintf.md index 3727b900ee..2494d2c5ae 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 458 --- ## `vsprintf()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/vsprintf.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 84bb6244a3..31c9dc8a73 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 432 + order: 459 --- ## `wordwrap()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/wordwrap.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` @@ -26,7 +26,7 @@ sidebar: - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `may_alias_arguments` -- **Effects**: `static (0 declared effects)` +- **Effects**: `static (1 declared effects)` - **Requirements**: `static (0 requirements)` - **Callable policy**: `static_only` - **Target support**: `macos-aarch64`, `linux-aarch64`, `linux-x86_64` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 283f5f963c..8fec9c26e6 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 433 + order: 460 --- ## `boolval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_alnum.md b/docs/internals/builtins/type/ctype_alnum.md index 03dc8288d6..21a1ddda3c 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 434 + order: 461 --- ## `ctype_alnum()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alnum.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_alpha.md b/docs/internals/builtins/type/ctype_alpha.md index 6e48c2e492..31b255b05d 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 435 + order: 462 --- ## `ctype_alpha()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_alpha.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_digit.md b/docs/internals/builtins/type/ctype_digit.md index 6d4b0959af..90f4550b4a 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 436 + order: 463 --- ## `ctype_digit()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_digit.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/ctype_space.md b/docs/internals/builtins/type/ctype_space.md index 030464d45d..365015837e 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 437 + order: 464 --- ## `ctype_space()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/ctype_space.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 9906afcbe7..e7edf72f81 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 438 + order: 465 --- ## `floatval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index 90f0a5d915..c90664c6f3 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 439 + order: 466 --- ## `get_resource_id()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_id.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index 891ae663a1..97643ed004 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 440 + order: 467 --- ## `get_resource_type()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_type.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index e70eb8cc0f..3a2f3b06e3 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 441 + order: 468 --- ## `gettype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/gettype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index d86f03e2b9..40e0265d01 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 442 + order: 469 --- ## `intval()` — internals @@ -10,18 +10,18 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` ### Lowering notes -- Uses the `eir_primitive` strategy from the single-source builtin descriptor. +- Uses the `eir_graph` strategy from the single-source builtin descriptor. - Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`. ## Semantic descriptor -- **Target strategy**: `eir_primitive` +- **Target strategy**: `eir_graph` - **Validation**: `signature` - **Result type source**: `declared` - **Result ownership**: `non_heap` @@ -37,12 +37,12 @@ sidebar: ## Signature summary ```php -function intval(mixed $value): int +function intval(mixed $value, int $base = 10): int ``` ## What the type checker enforces -- **Arity**: takes exactly 1 argument. +- **Arity**: takes 1–2 arguments (1 optional). ## Eval interpreter (magician) diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index af3c32ab8d..c34846ee9e 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 443 + order: 470 --- ## `is_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index db6bd7ba3c..62152a758b 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 444 + order: 471 --- ## `is_bool()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index 882b9b3f43..2af8973738 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 445 + order: 472 --- ## `is_callable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_double.md b/docs/internals/builtins/type/is_double.md index fb9d709f16..5fe3cb2234 100644 --- a/docs/internals/builtins/type/is_double.md +++ b/docs/internals/builtins/type/is_double.md @@ -2,7 +2,7 @@ title: "is_double() — internals" description: "Compiler internals for is_double(): lowering path, type checks, and runtime helpers." sidebar: - order: 446 + order: 473 --- ## `is_double()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_double.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_double.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 5a0c06b689..8d84f1346c 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 447 + order: 474 --- ## `is_float()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 59aaf52bac..c904256e40 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 448 + order: 475 --- ## `is_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_integer.md b/docs/internals/builtins/type/is_integer.md index ba54247f98..fb8d3689f7 100644 --- a/docs/internals/builtins/type/is_integer.md +++ b/docs/internals/builtins/type/is_integer.md @@ -2,7 +2,7 @@ title: "is_integer() — internals" description: "Compiler internals for is_integer(): lowering path, type checks, and runtime helpers." sidebar: - order: 449 + order: 476 --- ## `is_integer()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_integer.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_integer.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index f3ff6bcc89..1aa1f9847b 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 450 + order: 477 --- ## `is_iterable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_long.md b/docs/internals/builtins/type/is_long.md index aa8110055a..40c16c6f00 100644 --- a/docs/internals/builtins/type/is_long.md +++ b/docs/internals/builtins/type/is_long.md @@ -2,7 +2,7 @@ title: "is_long() — internals" description: "Compiler internals for is_long(): lowering path, type checks, and runtime helpers." sidebar: - order: 451 + order: 478 --- ## `is_long()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_long.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_long.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index 675fd7945c..2c59857d03 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 452 + order: 479 --- ## `is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_numeric.md b/docs/internals/builtins/type/is_numeric.md index 68d7909be7..c6642b4506 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 453 + order: 480 --- ## `is_numeric()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_numeric.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index 72ed3e42b2..b9ca046c78 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 454 + order: 481 --- ## `is_object()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_real.md b/docs/internals/builtins/type/is_real.md index fb35a937e9..7eb6880d73 100644 --- a/docs/internals/builtins/type/is_real.md +++ b/docs/internals/builtins/type/is_real.md @@ -2,7 +2,7 @@ title: "is_real() — internals" description: "Compiler internals for is_real(): lowering path, type checks, and runtime helpers." sidebar: - order: 455 + order: 482 --- ## `is_real()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_real.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_real.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 88b24c22db..c31720c216 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 456 + order: 483 --- ## `is_resource()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_resource.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index 3992075147..fbfce16681 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 457 + order: 484 --- ## `is_scalar()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index 6a044725b0..9a8028e2c7 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 458 + order: 485 --- ## `is_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/settype.md b/docs/internals/builtins/type/settype.md index bfde8d1b39..df7af26700 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 459 + order: 486 --- ## `settype()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/settype.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/builtins/type/strval.md b/docs/internals/builtins/type/strval.md index a57fc34d89..ce5d873777 100644 --- a/docs/internals/builtins/type/strval.md +++ b/docs/internals/builtins/type/strval.md @@ -2,7 +2,7 @@ title: "strval() — internals" description: "Compiler internals for strval(): lowering path, type checks, and runtime helpers." sidebar: - order: 460 + order: 487 --- ## `strval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/strval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/strval.rs) -- **Lowering**: [`src/builtins/semantics.rs`:448](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L448) (`lower_registry_call`) +- **Lowering**: [`src/builtins/semantics.rs`:540](https://github.com/illegalstudio/elephc/blob/main/src/builtins/semantics.rs#L540) (`lower_registry_call`) - **Function symbol**: `lower_registry_call()` diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index 1f31fbd887..581166174c 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -161,8 +161,11 @@ b.eq value_is_null A tagged null carries the sentinel as its payload word, so boxing it into a Mixed cell produces `{tag 8, sentinel}` words and un-audited consumers degrade -to sentinel behavior. `?int` parameters, returns, and properties keep their boxed Mixed -representation under both modes. +to sentinel behavior. Declared `?int` parameters, returns, and properties are tagged +scalars too under the default mode, so they round-trip the full 64-bit range; a `?int` +property slot stores the payload at its slot offset and the runtime tag at `offset + 8`, +and its literal default must be written as that same `{payload, tag}` pair rather than as +a pointer to a boxed Mixed cell. ### Pointer values @@ -347,6 +350,8 @@ Header (24 bytes) │ ptr[0] (8B) │ len[0] (8B) │ ptr[1] (8B) │ len[1] (8B Access: `base + 24 + (index × 16)` for pointer, `base + 24 + (index × 16) + 8` for length +The wider slot is why a runtime helper that walks an indexed array cannot assume 8-byte payloads. `array_splice()` used to do exactly that on a string receiver, so the removed-elements array came back holding raw heap pointers surfaced as PHP integers; string arrays now go through the dedicated `__rt_array_splice_str` / `__rt_array_splice_insert_str` pair. A string array also owns its payloads exclusively — a copy-on-write split re-persists every slot and a deep free releases every slot — so moving a slot between two string arrays transfers ownership with it, while inserting one duplicates it with `__rt_str_persist`. + ### Array growth When `array_push` finds that `length >= capacity`, the array grows automatically: @@ -369,6 +374,8 @@ Indexed arrays and associative arrays now follow **shared-until-modified** seman This is what lets PHP-style code such as `$b = $a; $b[0] = 9;` leave `$a` unchanged without requiring deep copies on every assignment. Nested arrays and hashes remain shallow-shared until their own first mutation. +Because both the split in step 4 and any growth relocate the container, a mutating builtin has to publish the new pointer back into the place its receiver was READ from. A plain local is its own frame slot; a **by-reference parameter** is read through a reference cell and must be republished through that cell. Missing that write-back is not a leak but a wrong answer: the caller keeps the pre-split container (so the mutation is invisible) or, after a growth, a pointer into storage the reallocation already freed. + ## Hash table layout (associative arrays) Associative arrays use a separate heap-allocated structure: an open-addressing hash table for lookup plus an insertion-order linked list threaded through the entries. @@ -622,7 +629,10 @@ The naming pattern comes from `static_property_symbol(...)`. Inherited static pr | User globals | 16 bytes per `global $var` slot | Grows with number of referenced globals | | Static vars | 24 bytes per `static $var` (`16 + 8 init flag`) | Grows with number of declared static locals | | Static properties | 16 bytes per effective declaring class static property | Grows with number of declared and redeclared static properties | -| Array capacity | Fixed at creation until grow/re-hash logic runs | Fatal error: "array capacity exceeded" if a hard limit is hit | +| Array capacity | Fixed at creation until grow/re-hash logic runs | `__rt_array_new` / `__rt_hash_new` validate the requested size first: negative capacities allocate an empty payload region, and a `capacity * elem_size` (or `capacity * 64`) that does not fit in a non-negative machine word aborts with "Fatal error: requested array size exceeds the maximum allowed array size" instead of wrapping to a small allocation | +| Range element count | `range()` sizes its result from `\|end - start\| + 1` | An interval needing more than 1073741823 elements never reaches the allocator: the lowering guard raises reference PHP's catchable `ValueError` ("The supplied range exceeds the maximum array size: start=… end=… step=…") first, naming the ordered endpoints and `abs($step)`. The runtime's own overflow abort remains as the backstop | +| Fill element count | `array_fill()` sizes its result from `$count` | A `$count` past `INT_MAX` (2147483647) never reaches the allocator: the lowering guard raises reference PHP's catchable `ValueError` ("array_fill(): Argument #2 ($count) is too large"). Counts at or below the bound whose payload does not fit the heap still report "Fatal error: heap memory exhausted", which is what reference PHP reports as a memory-limit fatal too | +| Buffer length | Fixed at `buffer_new()` | A negative length, or a `length * stride` that does not fit in a non-negative machine word, aborts with "Fatal error: buffer_new() length is negative or exceeds the maximum buffer size" | | C-string buffers | `_cstr_buf`, `_cstr_buf2` = 4KB each, `_empty_str` = 1 byte | Long converted paths/strings are truncated to buffer size; `_empty_str` is a safe zero-length string pointer | | File descriptor state | `_eof_flags`, `_stream_read_filters`, `_stream_write_filters` = 256 bytes each; `_popen_files`, `_dir_handles`, `_glob_handles`, `_zstream_handles`, `_bzstream_handles`, `_iconv_handles`, `_tls_sessions`, `_stream_chunk_size` = 2048 bytes each | Per-fd stream, process, directory, compression, iconv, TLS, and chunk-size bookkeeping for up to 256 descriptors | | Stream filter scratch | `_stream_filter_buf`, `_stream_grow_scratch` = 64KB each | Scratch space for stream filters, including length-growing filters such as base64 and quoted-printable encoders | diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index b44c71a15f..86ba05000d 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -162,6 +162,30 @@ resolved through native class-id branches. `global` variables load and store through `_eir_global_` symbols emitted for the EIR `LoadGlobal` / `StoreGlobal` instructions. +### Call-stack overflow guard + +`src/codegen/stack_guard.rs` emits a stack-depth check into every compiled +function, method, closure, and generator body prologue, immediately after the +frame has been reserved (so the compare already accounts for that frame) and +before the incoming parameters are spilled. It is an unsigned comparison of the +stack pointer against the runtime `_stack_limit` word, branching to +`__rt_stack_overflow` when the pointer is below it: + +- **AArch64** (macOS and Linux): `adrp` / `ldr` / `cmp sp, x9` / `b.hs ` / + `b __rt_stack_overflow` — five instructions, clobbering only `x9`. The + conditional branch stays local because `b.cond` only encodes a ±1 MiB + displacement, which large programs exceed; the unconditional `b` reaches + ±128 MiB and gets linker veneers beyond that. +- **x86_64**: `cmp rsp, QWORD PTR [rip + _stack_limit]` / `jb __rt_stack_overflow` + — two instructions, clobbering nothing. + +The check never writes memory, so it is safe to run when the remaining stack is a +single page. A zero `_stack_limit` makes it always pass, which is the state +before `__rt_stack_limit_init` runs and whenever the stack bounds could not be +determined — the guard is inert rather than wrong. `main` is not guarded: it is +the root of every call chain and runs before the floor exists. See +[The runtime](the-runtime.md) for the floor measurement and the fiber handoff. + ### Sentinels and null representation `src/codegen_support/sentinels.rs` is the canonical home for the in-band diff --git a/docs/internals/the-optimizer.md b/docs/internals/the-optimizer.md index 6e1debfb9c..f88f21facb 100644 --- a/docs/internals/the-optimizer.md +++ b/docs/internals/the-optimizer.md @@ -54,16 +54,16 @@ By the time codegen sees this, it can already emit constants instead of calling Current folding coverage includes: -- scalar arithmetic: `+`, `-`, `*`, `/`, `%`, `**` -- bitwise and shift ops on integers +- scalar arithmetic: `+`, `-`, `*`, `/`, `%`, `**`, keeping PHP's result *types* and overflow rules — `6 / 3` stays `int(2)` while `7 / 2` becomes `float(3.5)`, `2 ** 3` stays `int(8)`, and results that leave the `i64` range (`PHP_INT_MAX + 1`, `-PHP_INT_MIN`, `PHP_INT_MIN / -1`) promote to float. Operations PHP turns into a runtime error (`% 0`, `/ 0`, a negative shift count) are left unfolded so the error still happens. +- bitwise and shift ops on integers, including PHP's out-of-width shift results (`1 << 64` is `0`, `-1 >> 64` is `-1`) - unary `-`, `!`, and `~` - string-literal concatenation with `.` -- strict comparisons and numeric comparisons +- comparisons (`==`, `!=`, `===`, `!==`, `<`, `>`, `<=`, `>=`) through a reimplementation of PHP 8's `zend_compare()` in `src/optimize/fold/compare.rs`. Integers are compared as integers rather than through `f64`, numeric strings are classified with PHP's `is_numeric_string()` grammar, and a number against a non-numeric string follows PHP 8's stringify-and-compare rule. A float against a non-numeric string is left unfolded because the answer depends on float-to-string formatting. - logical `&&` / `||` when both sides are scalar constants - spaceship `<=>` - `??`, ternary, and `match` when the selected result is already known -- scalar indexed and associative array-literal reads such as `[2, 9][0]` and `["a" => 2]["a"]` when every literal entry is scalar -- scalar casts such as `(int)"42"` or `(bool)"0"` when the semantics are unambiguous +- scalar indexed and associative array-literal reads such as `[2, 9][0]` and `["a" => 2]["a"]` when every literal entry is scalar. Keys are normalized with PHP's array-key rules first (`false` and `0` are the same slot, `"1"` is the integer `1`, `null` is `""`), and duplicate normalized keys are last-wins. A float key PHP would report as a lossy implicit conversion is left unfolded so the deprecation still fires. +- scalar casts such as `(int)"42"` or `(bool)"0"`. String-to-number casts use PHP's leading-numeric-prefix grammar, so `(int)"12abc"` is `12` and `(float)"INF"` is `0` — PHP's numeric strings have no `INF`, `NAN`, hexadecimal, or `_` separator forms. - recursive folding inside: - function and method bodies - closures and arrow functions diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index 8f18f58083..0aec050f22 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -162,7 +162,7 @@ Each `Stmt` also carries a source `span` and an `attributes` list. The list is p Constructor property promotion is normalized during class-body parsing. A parameter such as `public int $id` in `__construct` becomes a `ClassProperty` plus a synthetic leading `PropertyAssign` statement equivalent to `$this->id = $id;`. Parameter defaults stay on the constructor signature rather than `ClassProperty.default`, matching PHP's distinction between promoted parameter defaults and property defaults. By-reference promoted parameters preserve a `by_ref` flag on the generated property so codegen can bind the property slot to the referenced argument or to a heap reference cell when a default value is used. Later passes otherwise see ordinary properties and ordinary constructor assignments. -There is no `Declare` statement kind. `declare(directive=literal, ...)` is parsed by `src/parser/stmt/declare.rs` and lowered directly to `Synthetic`: the statement form `declare(strict_types=1);` becomes an empty `Synthetic(vec![])`, while the block form `declare(ticks=1) { ... }` (single statement, braced block, or alternative `: ... enddeclare;` syntax) becomes a `Synthetic` wrapping the body statements so they execute in the enclosing scope. Directive values must be literals; the parser enforces PHP's rules that `strict_types` takes only `0` or `1`, must be the very first statement in the script, and cannot use block mode. The directives themselves are compile-time syntax only, because elephc always uses strict typing. +There is no `Declare` statement kind. `declare(directive=literal, ...)` is parsed by `src/parser/stmt/declare.rs` and lowered directly to `Synthetic`: the statement form `declare(strict_types=1);` becomes an empty `Synthetic(vec![])`, while the block form `declare(ticks=1) { ... }` (single statement, braced block, or alternative `: ... enddeclare;` syntax) becomes a `Synthetic` wrapping the body statements so they execute in the enclosing scope. Directive values must be literals; the parser enforces PHP's rules that `strict_types` takes only `0` or `1`, must be the very first statement in the script, and cannot use block mode. `ticks` and `encoding` are compile-time syntax only, but `strict_types` is applied: once the directive passes those checks, the parser records it on its per-file source profile (`crate::source::declare_strict_types`), which stamps `Stmt::strict_types` on every statement parsed afterwards. Because PHP requires the directive to be the file's first statement, "afterwards" is exactly "the rest of this file", and the flag survives include/autoload merging so the type checker can pick strict or coercive parameter binding per call site (`src/types/param_binding.rs`). ### Statement dispatch diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 43b6be714d..86ccd8b771 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -106,11 +106,34 @@ Formats the native resource payload used by stream handles as PHP's display stri `__rt_resource_write_stdout` uses the same display form for `echo` / `print` without exposing the raw file descriptor as an integer. -### `__rt_ftoa` — Float to string +### `__rt_ftoa` — Float to string (`precision = 14`) **File:** `strings/ftoa.rs` -Converts a double-precision float in `d0` to a decimal string. Handles special cases: `INF`, `-INF`, `NAN`. For normal numbers, it separates the integer and fractional parts, converts each to digits, and joins them with a decimal point. +Converts a double-precision float in `d0` to the decimal string PHP produces for `echo`, +`(string)`, string interpolation, concatenation and `print_r()` — that is, +`zend_gcvt(value, 14, '.', 'E')`. It formats the value with `snprintf("%.14G", …)` into a +stack scratch buffer, then copies the bytes into the [concat buffer](memory-model.md#the-string-buffer-scratch-pad) +applying the two fixups where C's `%G` differs from `zend_gcvt`: exponential form always +keeps a mantissa fraction (`1.0E+300`, not `1E+300`) and the exponent is written without +zero padding (`1.0E-7`, not `1E-07`). `NAN` is emitted unsigned, since glibc renders a +negative quiet NaN as `-NAN` and PHP never does. `INF` / `-INF` pass through unchanged. + +**Input:** `d0` = float value +**Output:** `x1` = pointer to string, `x2` = length + +### `__rt_ftoa_repr` — Float to string (`serialize_precision = -1`) + +**File:** `strings/ftoa.rs` + +The rendering `var_dump()` uses: the shortest decimal string that round-trips back to the +same `double`, with an uppercase `E`, a `d.d` mantissa in exponential form, an unpadded +exponent, and no trailing `.0` for integer-valued floats. The plain/exponential boundary +is `zend_gcvt`'s for `ndigit = 17` (`decpt < -3 || decpt > 17`), which keeps +`var_dump(1e16)` as `10000000000000000` where `echo 1e16` is already `1.0E+16`. Finite +values are handed to the shared `__rt_json_ftoa` shortest-round-trip formatter (the one +`json_encode`/`serialize` use) with `'E'` as the exponent marker; this helper only owns +the `INF` / `-INF` / `NAN` spellings. **Input:** `d0` = float value **Output:** `x1` = pointer to string, `x2` = length @@ -161,7 +184,8 @@ Each routine follows the same pattern — inputs in registers, output in standar | Routine | What it does | Input | Output | |---|---|---|---| | `__rt_strcopy` | Copy string into concat buffer | `x1`/`x2` | `x1`/`x2` | -| `__rt_str_to_number` | Parse a PHP numeric string for loose comparison and numeric-string casts | `x1`/`x2` | numeric payload + success flag | +| `__rt_php_num_scan` | Clip a C string to PHP's leading numeric run (`_is_numeric_string_ex` grammar) in place and report whether the whole string was numeric. Runs between `__rt_cstr` and libc so `strtod`/`strtoll` never see hexadecimal, `INF`/`NAN` or underscore forms PHP does not accept | `x0` = C string | `x0` = run pointer, `x1` = fully-numeric flag | +| `__rt_str_to_number` | Parse a PHP numeric string for loose comparison, `is_numeric()`, and numeric-string casts (via `__rt_php_num_scan`) | `x1`/`x2` | numeric payload + success flag | | `__rt_str_looks_like_int_for_coercion` | Validate PHP coercive int-parameter numeric strings while rejecting libc-only `strtod` forms such as `0x`, `INF`, and `NAN` | `x1`/`x2` | `x0` (0 or 1) | | `__rt_str_to_int` | Parse a PHP numeric-string prefix with integer/float forms and truncate toward zero like PHP `(int)` casts | `x1`/`x2` | `x0` (integer) | | `__rt_str_loose_eq` | Compare two strings using PHP loose-comparison numeric-string rules before falling back to bytes | two strings | `x0` (0 or 1) | @@ -176,6 +200,8 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_mb_strlen` | Multibyte-aware string length for `mb_strlen()` (emitted only for programs that use it) | `x1`/`x2` | `x0` | | `__rt_strpos` | Find substring | `x1`/`x2` + `x3`/`x4` | `x0` (index or -1) | | `__rt_strrpos` | Find last occurrence | `x1`/`x2` + `x3`/`x4` | `x0` | +| `__rt_stripos` | Find substring, ASCII case-insensitive | `x1`/`x2` + `x3`/`x4` | `x0` (index or -1) | +| `__rt_strripos` | Find last occurrence, ASCII case-insensitive | `x1`/`x2` + `x3`/`x4` | `x0` | | `__rt_str_repeat` | Repeat N times with heap fallback for large results | `x1`/`x2` + count | `x1`/`x2` | | `__rt_str_replace` | Replace all occurrences | search + replace + subject | `x1`/`x2` | | `__rt_explode` | Split by delimiter | delimiter + string | `x0` (array ptr) | @@ -194,7 +220,8 @@ Each routine follows the same pattern — inputs in registers, output in standar | `__rt_sha1` | SHA1 hash | `x1`/`x2` | `x1`/`x2` | | `__rt_sprintf` | Format string | format + args on stack | `x1`/`x2` | | `__rt_base64_encode` | Base64 encode | `x1`/`x2` | `x1`/`x2` | -| `__rt_base64_decode` | Base64 decode | `x1`/`x2` | `x1`/`x2` | +| `__rt_base64_decode` | Base64 decode (php-src semantics, `$strict` in `x3`) | `x1`/`x2`/`x3` | `x0` ok flag + `x1`/`x2` | +| `__rt_quoted_printable_encode` | MIME quoted-printable encode | `x1`/`x2` | `x1`/`x2` | | `__rt_urlencode` | URL encode | `x1`/`x2` | `x1`/`x2` | | `__rt_urldecode` | URL decode | `x1`/`x2` | `x1`/`x2` | | `__rt_htmlspecialchars` | HTML escape | `x1`/`x2` | `x1`/`x2` | @@ -340,6 +367,8 @@ See [Memory Model](memory-model.md) for the hash table memory layout. | `__rt_array_merge` | Concatenate two indexed arrays into a new array | | `__rt_array_merge_into` | Append all elements from source array into dest array (in-place) | | `__rt_array_slice` / `__rt_array_splice` | Extract slices and remove splice windows from indexed arrays | +| `__rt_array_splice_str` | The `array_splice()` removal for indexed **string** arrays, whose payload slots are 16-byte `{pointer, length}` pairs rather than the 8-byte slots the other splice helpers move. The removed strings are MOVED into the result array: an indexed string array owns its persisted bytes exclusively, so retaining them would double free and copying them would leak | +| `__rt_array_splice_insert` / `_refcounted` / `_boxed` / `_unboxed` / `_str` | Write `array_splice()`'s `$replacement` into the gap the removal opened, growing the destination first. The five variants differ in what one replacement slot becomes: copied verbatim, retained, wrapped in a fresh boxed `Mixed` cell, read back out of one as a plain integer, or duplicated with `__rt_str_persist` into a 16-byte string slot | | `__rt_array_unique` | Remove duplicate values | | `__rt_array_diff` / `__rt_array_intersect` | Set difference/intersection by value | | `__rt_array_diff_key` / `__rt_array_intersect_key` | Set operations by key | @@ -355,17 +384,22 @@ See [Memory Model](memory-model.md) for the hash table memory layout. | `__rt_range` | Generate integer range array | | `__rt_shuffle` / `__rt_array_rand` | Randomize order / pick random | | `__rt_random_u32` / `__rt_random_uniform` | Target-aware random primitives used by `rand()`, `random_int()`, `shuffle()`, and `array_rand()` | -| `__rt_asort` / `__rt_arsort` | Sort by value while preserving keys, ascending or descending | -| `__rt_ksort` / `__rt_krsort` | Sort by key, ascending or descending | +| `__rt_asort` / `__rt_arsort` | Sort an indexed array by value, ascending or descending | +| `__rt_hash_ksort` / `__rt_hash_krsort` | Sort an associative array by key, ascending or descending | +| `__rt_hash_asort` / `__rt_hash_arsort` | Sort an associative array by value while preserving keys, ascending or descending | +| `__rt_hash_sort_links` | Shared engine behind the four hash sorts: a stable insertion sort that relinks the table's `prev`/`next`/`head`/`tail` chain, so buckets never move, key/value association is preserved, and no refcount changes | +| `__rt_hash_sort_triple` | Reads a hash entry's key or value as a `__rt_php_compare` `(tag, lo, hi)` triple, peeling boxed Mixed cells | | `__rt_natsort` / `__rt_natcasesort` | Natural-order sort, case-sensitive or case-insensitive | | `__rt_array_map` | Apply callback to each scalar element, return new array; an optional third argument carries a captured-closure environment for generated callback wrappers | | `__rt_array_map_str` | Apply callback to each scalar or string element and return a string array; an optional third argument carries a captured-closure environment | | `__rt_array_map_str_owned` | Apply a descriptor-wrapper callback that returns owned strings and transfer those strings directly into the result array | | `__rt_array_map_mixed` | Apply a descriptor-backed callback that returns owned boxed Mixed cells and store them directly into a newly allocated result array | | `__rt_array_filter` | Filter scalar elements where callback returns truthy; an optional third argument carries a captured-closure environment | -| `__rt_array_reduce` | Reduce array to single value via callback; an optional fourth argument carries a captured-callback environment | +| `__rt_array_reduce` | Reduce an indexed array of 8-byte payload slots to a single value via callback; an optional fourth argument carries a captured-callback environment | +| `__rt_array_reduce_str` | Reduce an indexed string array's 16-byte `[ptr][len]` slots into one integer accumulator, passing each element to the callback as a pointer/length pair; an optional fourth argument carries a captured-callback environment | | `__rt_array_walk` | Call callback on each element (side-effects); an optional third argument carries a captured-callback environment | -| `__rt_usort` | Sort array using user comparison callback; an optional third argument carries a captured-callback environment | +| `__rt_usort` | Sort an indexed array of 8-byte payload slots using a user comparison callback; an optional third argument carries a captured-callback environment | +| `__rt_usort_str` | Stable insertion sort over an indexed string array's 16-byte `[ptr][len]` slots using a user comparison callback that receives both strings as pointer/length pairs; an optional third argument carries a captured-callback environment | ### Reference counting @@ -385,6 +419,40 @@ See [Memory Model](memory-model.md) for the hash table memory layout. Refcounts are stored as a 32-bit value in the uniform 16-byte heap header, at `[user_ptr - 12]`. Each heap allocation starts with refcount 1. When a reference is shared (e.g., assigned to another variable or passed to a function), `__rt_incref` bumps it. When the reference goes away, `__rt_decref_any` can dispatch through the uniform heap-kind tag to the concrete string/array/hash/object/mixed release path. Runtime-thrown Throwable payloads carry the dedicated heap kind `6`, which `__rt_decref_any` and `__rt_object_free_deep` accept and route through the same object release path (issue #448). Arrays, hashes, objects, and boxed mixed cells still use ordinary reference counting first, but when a decref sees a container/object graph that can contain nested heap-backed values, the runtime can invoke `__rt_gc_collect_cycles` to clear transient metadata, count heap-only incoming edges, mark externally reachable blocks, and deep-free the remaining unreachable array/hash/object/mixed island. +## Loose-equality routines + +**Source:** `src/codegen_support/runtime/compare/` + +PHP's `==` is decided at run time whenever the static operand types do not settle +it: two boxed `mixed` cells, two arrays, or two objects. Three mutually recursive +helpers implement PHP 8's comparison table; the backend's `lower_loose_eq` +fallback boxes both operands and calls the first one. + +| Routine | What it does | Input | Output | +|---|---|---|---| +| `__rt_mixed_loose_eq` | PHP `==` between two boxed mixed values, entering at recursion depth 0 | `x0`/`x1` = mixed pointers | `x0` = 0/1 | +| `__rt_mixed_loose_eq_d` | Same, carrying an explicit recursion depth so the walkers stay reentrant | `x0`/`x1` = mixed pointers, `x2` = depth | `x0` = 0/1 | +| `__rt_mixed_array_loose_eq` | Equal counts plus, for every key of the left array, the same key on the right with a loosely equal value (order-independent) | `x0`/`x1` = boxed arrays, `x2` = depth | `x0` = 0/1 | +| `__rt_obj_loose_eq` | Same instance, or same runtime class id with every descriptor property loosely equal | `x0`/`x1` = object pointers, `x2` = depth | `x0` = 0/1 | + +Rule order inside `__rt_mixed_loose_eq` is load-bearing: a `bool` operand coerces +both sides first, then `null` (converted to `""` against a string and to `bool` +otherwise), then containers (an array equals only another array), then objects, +then strings, then a numeric comparison. Same-tag int/resource/callable payloads +compare word-for-word so large integers keep full precision. + +Both operands stay borrowed. The array walker reads elements through +`__rt_mixed_array_get` and the object walker reads properties through +`__rt_obj_prop_value`; both return OWNED cells, and both walkers release them +after each comparison. Key presence is probed (`__rt_hash_get`, or the list bounds +for tag-4 arrays) before a value is read, because a missing key and a stored +`null` are otherwise indistinguishable. + +The depth argument caps recursion (`MAX_LOOSE_EQ_DEPTH`): a cyclic object/array +graph reports "not equal" instead of running the stack out. PHP instead raises +`Nesting level too deep - recursive dependency?`, which the runtime has no unwind +path for from a leaf helper. + ## System routines **Source:** `src/codegen_support/runtime/system/` (43 top-level files plus `date/`, `strtotime/`, `json_validate/`, `json_decode_mixed/`, and `json_encode_str/` subdirectories; 70 files recursively) @@ -409,6 +477,32 @@ At program start, the OS passes `argc` (argument count) in `x0` and `argv` (poin | `__rt_php_uname` | Read target runtime system information via libc `uname()`; supports PHP modes `a`, `s`, `n`, `r`, `v`, and `m` | `x1`/`x2` = mode string | `x1`/`x2` = selected uname string | | `__rt_shell_exec` | Execute shell command and capture output via libc `popen()`/`pclose()` | `x1`/`x2` = command string | `x1`/`x2` = output string | +### Call-stack overflow guard + +**File:** `system/stack_guard.rs` + +Unbounded recursion would otherwise run the stack pointer off the end of the mapping and kill the process with a raw `SIGSEGV`. Two runtime helpers plus one word of state turn that into a controlled fatal on every supported target. + +| Routine | What it does | Input | Output | +|---|---|---|---| +| `__rt_stack_limit_init` | Measure the running stack once and publish the lowest address compiled prologues may reach | — | writes `_stack_limit` and `_stack_limit_main` | +| `__rt_stack_overflow` | Write `Fatal error: Maximum call stack size reached. Infinite recursion?` to stderr and exit with status 255 | — | does not return | + +`__rt_stack_limit_init` calls `getrlimit(RLIMIT_STACK, …)` — resource number 3 on both Linux and macOS — and publishes `entry_sp - (min(rlim_cur, 64 MiB) - 32 KiB)`. The cap absorbs `RLIM_INFINITY`; the 32 KiB reserve is the headroom a guarded frame may still consume before the next guarded call (outgoing stack arguments, `__rt_*` helper frames, and their libc calls). When `getrlimit` fails, reports an implausibly small limit, or the subtraction would wrap, the routine publishes zero instead, and zero disables the guard for the whole process. + +The process-entry prologue calls it once, after argc/argv have been stored to globals (it is an ordinary call and clobbers the argument registers). Under `--web` the call sits in the process-entry stub, before the workers are forked, so every worker inherits a floor that matches its own stack. + +Two globals hold the state: + +| Symbol | Meaning | +|---|---| +| `_stack_limit` | Lowest stack address the *currently running* context may reach; `0` disables the guard | +| `_stack_limit_main` | The OS-thread floor, remembered so `__rt_fiber_switch` can restore it | + +Fibers and generators run on their own 256 KiB mmap'd coroutine stack, which has nothing to do with the OS-thread stack, so `__rt_fiber_switch` swaps `_stack_limit` along with the exception and cleanup chain heads: switching *into* a fiber publishes `stack_base + guard page + reserve`, and switching back to the main context restores `_stack_limit_main`. A fiber whose stack allocation failed publishes zero, leaving the guard inert rather than comparing against a nonsensical address. + +The check itself lives in every compiled function prologue — see [The codegen](the-codegen.md). + ## Exception routines **Source:** `src/codegen_support/runtime/exceptions.rs` plus `src/codegen_support/runtime/exceptions/` (7 files) diff --git a/docs/internals/the-type-checker.md b/docs/internals/the-type-checker.md index 2b46d27039..924396acc3 100644 --- a/docs/internals/the-type-checker.md +++ b/docs/internals/the-type-checker.md @@ -106,6 +106,12 @@ The first assignment determines a variable's type. After that, reassignment is o Declared boundaries are looser than plain reassignment. A `Mixed` value is accepted where a declared parameter, return, or property expects a plain `Int`, `Float`, `Bool`, or `Str` (PHP's coercive mode): the checker lets it through and codegen inserts the runtime unboxing/narrowing conversion. Union values are accepted member-wise — every member of the actual union must be accepted by some member of the expected type. +### Per-file `strict_types` + +`declare(strict_types=1)` is scoped to one physical file, but the checker only ever sees the single flat program the resolver produces after include/autoload merging, and `Span` carries no file identity. The flag therefore rides on the AST: the parser records the directive on its per-file source profile (`crate::source`), `Stmt::strict_types` inherits it at construction alongside `Stmt::source_mode`, and every statement-rewriting pass re-installs the whole `SourceProfile` — which is why `with_parse_mode`/`scoped_parse_mode` take the profile rather than the mode alone. + +`Checker::check_stmt` installs `Stmt::strict_types` on `Checker::strict_types` and restores the outer value afterwards, so the setting always reflects the file the *call site* was written in — matching PHP, where a strict file calling into a coercive one is strict and a coercive file calling a function declared in a strict one is not. `Checker::require_strict_types_param_binding` then runs *before* `types_compatible`, because the widenings PHP drops in strict mode (`bool`→`int`, `int`→`bool`, …) are ones `types_compatible` accepts on its own. `Checker::with_internal_callback_binding` suspends the flag while validating a callback that an internal function invokes (`array_map`, `usort`, …), which PHP calls from an engine frame that never carries the directive. + This means elephc rejects code that PHP would allow: ```php @@ -232,6 +238,53 @@ The type checker validates: 2. **Argument types** — wrong types → error (in some cases; many builtins accept multiple types) 3. **Return type** — used to infer the type of the call expression +### Contextual callback parameter typing + +**File:** `src/types/checker/builtins/callables.rs` + +An array builtin types the *unannotated* parameters of its callback from the array it is given, so +the idiomatic untyped closure/arrow function checks correctly: + +```php +$words = ["banana", "apple"]; +usort($words, fn($a, $b) => strlen($a) <=> strlen($b)); // $a, $b are string +``` + +`array_map` (argument 0), `array_all` / `array_any` / `array_filter` / `array_find` / +`array_reduce` / `array_walk` / `array_walk_recursive` / `uasort` / `uksort` / `usort` +(argument 1) and `array_udiff` / `array_uintersect` (argument 2) are typed this way. Value +parameters take the array's element type; key parameters take the array's key type — `Int` for an +indexed array, the declared key type for an associative one. That is what `uksort` compares, what +`array_filter` passes under `ARRAY_FILTER_USE_KEY` / `ARRAY_FILTER_USE_BOTH`, and what +`array_walk` passes as its optional second callback parameter (added only when the callback +literally declares it, so a one-parameter callback still satisfies arity checking). + +`contextual_callback_arg_positions()` is the single source of truth for those positions, and every +eager pre-inference pass consults it. Skipping them matters: inferring the closure before the hook +supplies its hints would check the body once against the unhinted parameter fallback and reject +valid code. Explicitly declared parameter types always stay authoritative, and an element type the +array does not pin down (`Mixed`/`Never`) leaves the parameter `Mixed`. + +### Null probes (`isset` / `empty` / `unset` / `??`) + +**File:** `src/types/checker/null_probe.rs` + +`isset()`, `empty()`, `unset()` and the left operand of `??` / `??=` exist to name storage that may +never have been declared, and PHP answers all of them without an `Undefined variable` warning. The +checker matches that: the *spine* of the operand's access chain (`$x`, `$x[...]`, `$x->p`, `$x?->p`) +may bottom out in an undeclared variable, which reads as `null`. Reaching through a `null` base is +allowed inside a probe too, so `isset($never['k'])` answers `false` instead of "Cannot index +non-array". Index and property-name subexpressions are *not* covered — PHP still warns about `$b` +in `isset($a[$b])`, so that keeps the ordinary diagnostic, as does every read outside a probe. + +Acceptance is decided at the *end* of the top-level pass rather than at the probe. EIR lowering +derives `main`'s local types from `CheckResult::global_env`, so a probed name is only representable +while it stays `null` for the whole scope: it must finish the pass unbound, and the checker then +seeds it as `null` so codegen answers from the slot type instead of reading storage no store ever +initializes. A name that is *also* assigned at top level (`if (!isset($cfg)) { $cfg = 3; }`) would +get that assigned type on a slot the probe reads before the store, so the original diagnostic is +restored for it. + ## User-defined function checking **Files:** `src/types/checker/functions.rs`, `src/types/checker/functions/` @@ -284,9 +337,27 @@ This information is then used when checking calls to that function. **File:** `src/types/checker/stmt_check/narrowing.rs` -Inside an `if` (or ternary) guarded by a type predicate, the checker narrows the guarded binding's type for each branch. `is_int`/`is_integer`/`is_long`, `is_float`/`is_double`/`is_real`, `is_string`, and `is_bool` narrow to the corresponding scalar; `$x instanceof Class` narrows to that class; `is_null($x)` and the strict comparisons `$x === null` and `$x === false` (in either operand order) narrow to `null` and to the literal `False` subtype respectively. The then-branch sees the guarded type and the else-branch sees the complement (a `Union` drops the matched members); a leading `!` swaps the two. The false-sentinel case preserves the literal `false`: after `if ($x === false) { throw ...; }`, an `int|false` value continues as plain `int`, while a full `bool` member is not stripped. +Inside an `if` (or ternary) guarded by a type predicate, the checker narrows the guarded binding's type for each branch. `is_int`/`is_integer`/`is_long`, `is_float`/`is_double`/`is_real`, `is_string`, and `is_bool` narrow to the corresponding scalar; `$x instanceof Class` narrows to that class; `is_null($x)` and the strict comparisons `$x === null` and `$x === false` (in either operand order) narrow to `null` and to the literal `False` subtype respectively. `$x !== null` / `$x !== false` and single-operand `isset($x)` are the same guards with the branches swapped, so a leading `!` on them cancels out. The then-branch sees the guarded type and the else-branch sees the complement (a `Union` drops the matched members); a leading `!` swaps the two. The false-sentinel case preserves the literal `false`: after `if ($x === false) { throw ...; }`, an `int|false` value continues as plain `int`, while a full `bool` member is not stripped. + +The guarded receiver may be a variable, a simple instance property (`$var->prop`, `$this->prop`), or a simple static property (`self::$p`, `Cls::$p`). `static::$p` is never narrowed, because late static binding can select a subclass that redeclares the property. Property narrowings are stored under a synthetic environment key and are conservatively dropped after anything that could mutate the property — a property assignment, any call, or loop-body entry — and dropped per-object when the root local is rebound. Properties backed by PHP 8.4 `get` hooks or `__get` are never narrowed, because two reads may produce different values. Guard detection never raises a diagnostic of its own: a receiver whose type cannot be inferred is simply not narrowed. + +#### Lazy initialization (the singleton shape) + +A completed `$this->p = ` or `self::$p = ` write re-establishes the fact for that place, recorded as the property's *declared* type minus `null` (never the assigned expression's type — a declared property coerces what it stores). When a single guarded clause both falls through and wrote its guarded place, the type after the `if` is the union of the then-branch exit fact and the guard complement instead of the pre-`if` type. Together those make PHP's lazy-initialization idiom check: + +```php +class S { + private static ?S $inst = null; + public static function get(): S { + if (self::$inst === null) { self::$inst = new S(); } // then-path: S + return self::$inst; // else-path: S => S + } +} +``` + +The `!isset(self::$inst)`, `self::$inst !== null` early-return, `self::$inst ??= new S()` and `$this->p ??= ...` spellings all reach the same fact. An intervening call still drops it, so a genuinely unsound program (`self::wipe(); return self::$inst;` — a `TypeError` under PHP) stays rejected. -The guarded receiver may be a variable or a simple property access (`$var->prop`, `$this->prop`). Property narrowings are stored under a synthetic environment key and are conservatively dropped after anything that could mutate the property — a property assignment, any call, or loop-body entry — and dropped per-object when the root local is rebound. Properties backed by PHP 8.4 `get` hooks or `__get` are never narrowed, because two reads may produce different values. +Return-type validation is flow-sensitive against these facts: each `return` records the type it had where it was checked (`Checker::flow_typed_returns`), so a narrowing established halfway down a body is not applied to a `return` that executes before it. Narrowing applies across `if`/`elseif`/`else` chains: each subsequent clause (and the `else`) sees the accumulated complement of the previous guards. A chain with no `else` whose every clause body always diverges (`return`, `throw`, `exit()`/`die()`, or a call to a function declared `: never`) narrows the statements after the entire `if` construct to the accumulated complement. This is what makes the common "overload" shape type-check: diff --git a/docs/php/arrays.md b/docs/php/arrays.md index ed9d28914b..e94e106d33 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -153,6 +153,33 @@ echo $b[0]; // 9 The same applies to function parameters and mutating built-ins (`array_push()`, `sort()`, `shuffle()`, etc.). +### Reference elements in array literals are not supported + +PHP lets an array literal hold a *reference* to a variable, so writing through the +array writes through to that variable: + +```php + &$a]`) and the legacy +`array(&$a)` spelling. elephc's arrays store plain values, and its only +reference form points *into* array storage (`$b =& $a[0]`), never out of it — +an element aliasing a local variable would be a pointer to a stack slot the +array can outlive. Assign the value and copy back afterwards, or alias an +existing element with `$b =& $a[0]`. + ## Multi-dimensional arrays ```php When a parameter is declared only as `array`, its element type is initially unknown. Array-callback checking preserves explicit callback parameter declarations and uses them to type the closure body instead of fabricating an `int` element. Known element types are still checked normally, and this contextual rule does not make `mixed` globally compatible with object, array, or other refcounted declarations. `array_map()` currently rejects known object-element arrays because its callback runtime does not yet support that input layout. > `call_user_func_array()` also accepts dynamic indexed and associative argument arrays for callbacks with a known signature, including userland variadic callbacks. When a callable value has no single static signature at the call site, elephc emits an AOT runtime dispatch over user functions and closure/FCC wrappers available in that codegen context, then applies the matched target's descriptor metadata: parameter names, defaults, by-reference flags, variadic position, return shape, captures, hidden receiver arguments, and callable shape. Runtime string callback names dispatch over user functions, supported builtins, and public static-method strings by case-insensitive name matching, materialize the matched descriptor, and invoke its generated descriptor invoker. Descriptor invokers receive a temporary boxed Mixed clone of the argument container and inspect its runtime tag to handle indexed arrays and associative hashes through the same signature-level wrapper, so the source `$args` remains usable with its original static layout after the call. String keys bind named parameters; unconsumed string and numeric keys are copied into `...$rest` for variadic callbacks. Dynamic arrays passed to by-reference callback parameters use temporary reference cells, so callback writes do not mutate the source argument array. -`usort()` and `uasort()` sort arrays of **objects** as well as scalars. The comparator receives each element as its object handle, so an unannotated comparator's parameters are typed from the array element automatically — `usort($items, fn($a, $b) => $a->weight <=> $b->weight)` works without writing `($a, $b)` type hints, and `usort($dates, fn($a, $b) => $a <=> $b)` over `DateTime`/`DateTimeImmutable` compares by instant. Explicit hints (`function (Item $a, Item $b)`) are equally accepted. Sorting an array of **strings** with a user comparator is not yet supported and reports a clear unsupported-feature error. +Unannotated callback parameters are typed from the array in every array builtin that takes a callback — `array_all()`, `array_any()`, `array_filter()`, `array_find()`, `array_map()`, `array_reduce()`, `array_udiff()`, `array_uintersect()`, `array_walk()`, `array_walk_recursive()`, `uasort()`, `uksort()` and `usort()`. Value parameters get the element type and key parameters get the key type, so `array_filter($words, fn($v) => strlen($v) > 3)`, `uksort($byName, fn($a, $b) => strlen($a) <=> strlen($b))` and `array_walk($byName, function ($v, $k) { echo strlen($k); })` all check without hand-written type hints. Explicit hints stay authoritative. + +### Sorting an associative array + +`ksort()`, `krsort()`, `asort()` and `arsort()` reorder an associative array by rewriting its +iteration order only — every key stays attached to its own value, later key lookups and inserts +keep working, and PHP's copy-on-write still applies, so a copy taken before the call keeps the +original order: + +```php +$byName = ["b" => 2, "a" => 3, "c" => 1]; +$snapshot = $byName; +ksort($byName); +echo implode(",", array_keys($byName)); // a,b,c +echo implode(",", array_keys($snapshot)); // b,a,c +``` + +Keys are ordered with PHP's standard comparison, not byte-wise, so numeric keys compare as +numbers even against string keys: `[10 => …, "9" => …, "Banana" => …]` sorts as `9`, `10`, +`'Banana'`. Ties keep their original relative order in every direction, matching PHP 8's stable +sorts — `["b" => 2, "d" => 2, "a" => 3]` keeps `b` before `d` under both `asort()` and `arsort()`. + +One known deviation: when an array mixes integer keys with string keys that are *not* numeric, +PHP's own key comparison is not transitive (for `10`, `"20a"` and `6`, PHP reports +`"20a" < 10`, `10 < 6`… and `6 < "20a"` is false), so no ordering satisfies every pair. In that +case elephc's result and PHP's result are both consistent with the comparison but can differ, +because each resolves the cycle through its own sort algorithm. + +`uasort()`, `uksort()`, `natsort()` and `natcasesort()` do not yet accept an associative array and +report a clear unsupported-feature error. + +`usort()` and `uasort()` sort arrays of **objects** as well as scalars. The comparator receives each element as its object handle, so an unannotated comparator's parameters are typed from the array element automatically — `usort($items, fn($a, $b) => $a->weight <=> $b->weight)` works without writing `($a, $b)` type hints, and `usort($dates, fn($a, $b) => $a <=> $b)` over `DateTime`/`DateTimeImmutable` compares by instant. Explicit hints (`function (Item $a, Item $b)`) are equally accepted. `usort()` also sorts arrays of **strings**: `usort($words, fn($a, $b) => strlen($a) <=> strlen($b))` reorders the string array in place, keeps elements the comparator reports equal in their original relative order, and renumbers the keys from zero like PHP. `uasort()` and `uksort()` over a string array still report a clear unsupported-feature error, because they must preserve the original key association. + +Array builtins that take their first argument **by reference** — `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, `natcasesort()`, `shuffle()`, `usort()`, `uasort()`, `uksort()`, `array_push()`, `array_pop()`, `array_shift()`, `array_unshift()`, `array_splice()` and `array_walk()` — mutate the caller's storage whether that storage is a local variable, an object property (`sort($obj->items)`, `sort($this->items)`, `sort($outer->inner->items)`), a static property (`sort(Foo::$items)`, `sort(self::$items)`), or a container element (`sort($rows[0])`, `sort($map["k"])`). PHP's copy-on-write applies as usual: a copy taken before the call keeps the original element order. + +```php +class Basket { public $items = [3, 1, 2]; } +$b = new Basket(); +$snapshot = $b->items; +usort($b->items, fn($x, $y) => $x <=> $y); +echo implode(",", $b->items); // 1,2,3 +echo implode(",", $snapshot); // 3,1,2 — the copy is untouched +``` + +A receiver elephc cannot resolve to writable storage — a nullsafe read (`sort($obj?->items)`, which PHP rejects too) or a property whose type is only known as `mixed` — is reported as a named unsupported-feature error rather than compiled into a silent no-op. + +The same applies when the receiver is a **by-reference parameter**: `function f(array &$a) { array_unshift($a, 1); }` mutates the caller's array, and so do `array_pop()`, `array_shift()`, `array_splice()`, the sort family, `array_multisort()` and an associative insert such as `$a["k"] = 1`. All of them copy-on-write split their receiver first, and prepending or splicing in a replacement can additionally relocate its storage, so the new pointer is published through the parameter's reference cell. + +`array_reduce()` folds arrays of **strings** too — `array_reduce($words, fn($carry, $word) => $carry + strlen($word), 0)` passes each element to the callback as a string. The accumulator itself must still be an `int` or `bool`; a string accumulator reports a clear unsupported-feature error. + +## The internal array pointer + +`key()`, `current()`, `next()`, `prev()`, `reset()` and `end()` operate on PHP's internal +array pointer: + +```php +$stock = ["apples" => 3, "pears" => 7, "plums" => 0]; + +reset($stock); +while (($qty = current($stock)) !== false) { + echo key($stock), "=", $qty, " "; // apples=3 pears=7 plums=0 + next($stock); +} + +echo end($stock); // 0 +echo key($stock); // plums +``` + +Semantics match PHP exactly for the supported receiver shape: + +- A freshly built array starts with its pointer on the first element, so `current()` and + `key()` work without calling `reset()` first. +- There is a single invalid position, and it is one-way. Running off the back with + `next()` or off the front with `prev()` leaves the pointer invalid; the opposite + direction does **not** walk back in. Only `reset()` and `end()` restore a valid pointer. +- While invalid, `current()`/`next()`/`prev()`/`reset()`/`end()` return `false` and `key()` + returns `null`. An empty array is always in that state. +- `foreach` never moves the pointer, by value or by reference — PHP 7+ iterates an + internal copy, and elephc's `foreach` keeps its cursor in the stack frame. +- Binding the variable to a different array rewinds its pointer to the first element, + because in PHP the pointer belongs to the hashtable that was replaced. +- Associative arrays are walked in insertion order and `key()` reports the real key. + +### Receiver must be a plain variable + +PHP stores the pointer inside the array's hashtable. elephc's array and hash headers have +no room for it — widening either would shift every offset in every runtime helper — so the +pointer lives in a hidden cursor slot the compiler allocates **beside the array local**. + +The direct consequence is that the argument must be a plain variable. A property, an array +element, a call result, or any other expression has nowhere to keep a cursor, so elephc +reports a compile error rather than silently operating on a detached one: + +```php +echo key($obj->rows); // compile error: key() argument must be an array variable +echo current(rows()); // compile error: current() argument must be an array variable +next($grid[0]); // compile error: next() argument must be an array variable +``` + +Copy the value into a local first (`$rows = $obj->rows;`) and walk that. + +### Known incompatibilities + +Because the cursor is attached to the variable instead of to the array value, three PHP +behaviours differ. All three involve a pointer that has been moved away from the first +element and then observed through a *different* variable. + +| Situation | PHP | elephc | +|---|---|---| +| `$a = [1,2,3]; next($a); $b = $a; echo key($b);` | `1` — the copy inherits the pointer at copy time | `0` — `$b` starts its own cursor | +| `function f($x) { return key($x); } $a = [1,2,3]; next($a); echo f($a);` | `1` — the by-value parameter inherits the caller's pointer | `0` — the parameter's cursor starts fresh | +| `$a = [3,1,2]; next($a); sort($a); echo key($a);` | `0` — `sort()`/`array_shift()`/`array_splice()` rewind the pointer | `1` — those builtins leave the cursor untouched | + +Everything else — including `$a[] = x` and `$a[$k] = v` **keeping** the pointer where it +is, which PHP also does — behaves the same. + +One performance note: reading through the cursor is `O(1)` for indexed arrays but walks +the insertion-order chain for associative arrays, so `current()`/`key()` on a hash cost +`O(position)`. A full `while (current(...)) { next(...); }` traversal of a large hash is +therefore quadratic; prefer `foreach` when you do not need the pointer. **Not supported yet:** `compact()` and `extract()` need dynamic access to the current variable scope. Magician's materialized named scope makes that behavior diff --git a/docs/php/builtins.md b/docs/php/builtins.md index 23db56a131..7dc0ad7148 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -11,9 +11,10 @@ sidebar: |---|---|---|:-:|:-:| | [`array_all()`](./builtins/array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | | [`array_any()`](./builtins/array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | -| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | | [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | | [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_count_values()`](./builtins/array/array_count_values.md) | `(array $array): array` | `array` | ✓ | ✓ | | [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | | [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | | [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | @@ -42,11 +43,11 @@ sidebar: | [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | | [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | -| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | | [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | | [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | -| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | -| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null, array $replacement = []): array` | `array` | ✓ | ✓ | | [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | | [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | | [`array_uintersect()`](./builtins/array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | @@ -60,12 +61,18 @@ sidebar: | [`call_user_func()`](./builtins/array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | | [`call_user_func_array()`](./builtins/array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | | [`count()`](./builtins/array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`current()`](./builtins/array/current.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`end()`](./builtins/array/end.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`key()`](./builtins/array/key.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`krsort()`](./builtins/array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`ksort()`](./builtins/array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`natcasesort()`](./builtins/array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`natsort()`](./builtins/array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`next()`](./builtins/array/next.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`prev()`](./builtins/array/prev.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end, int $step = 1): array` | `array` | ✓ | ✓ | +| [`reset()`](./builtins/array/reset.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | @@ -175,8 +182,8 @@ sidebar: | [`fgetc()`](./builtins/io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | | [`fgetcsv()`](./builtins/io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | | [`fgets()`](./builtins/io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | -| [`file()`](./builtins/io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | -| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./builtins/io/file.md) | `(string $filename, int $flags = 0): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename, bool $use_include_path = false, mixed $context = null, int $offset = 0, int $length = null): mixed` | `mixed` | ✓ | ✓ | | [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | | [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | | [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | @@ -267,15 +274,21 @@ sidebar: | [`asin()`](./builtins/math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`atan()`](./builtins/math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`atan2()`](./builtins/math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`base_convert()`](./builtins/math/base_convert.md) | `(string $num, int $from_base, int $to_base): string` | `string` | ✓ | ✓ | +| [`bindec()`](./builtins/math/bindec.md) | `(string $binary_string): mixed` | `mixed` | ✓ | — | | [`ceil()`](./builtins/math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`clamp()`](./builtins/math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | | [`cos()`](./builtins/math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`cosh()`](./builtins/math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`decbin()`](./builtins/math/decbin.md) | `(int $num): string` | `string` | ✓ | — | +| [`dechex()`](./builtins/math/dechex.md) | `(int $num): string` | `string` | ✓ | — | +| [`decoct()`](./builtins/math/decoct.md) | `(int $num): string` | `string` | ✓ | — | | [`deg2rad()`](./builtins/math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`exp()`](./builtins/math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`fdiv()`](./builtins/math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | | [`floor()`](./builtins/math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`fmod()`](./builtins/math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hexdec()`](./builtins/math/hexdec.md) | `(string $hex_string): mixed` | `mixed` | ✓ | — | | [`hypot()`](./builtins/math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | | [`intdiv()`](./builtins/math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | | [`is_finite()`](./builtins/math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | @@ -287,18 +300,20 @@ sidebar: | [`max()`](./builtins/math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | | [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | | [`mt_rand()`](./builtins/math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`octdec()`](./builtins/math/octdec.md) | `(string $octal_string): mixed` | `mixed` | ✓ | — | | [`pi()`](./builtins/math/pi.md) | `(): float` | `float` | ✓ | ✓ | | [`pow()`](./builtins/math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | | [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | | [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | -| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0, int $mode = 1): float` | `float` | ✓ | ✓ | | [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`tan()`](./builtins/math/tan.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`tanh()`](./builtins/math/tanh.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`buffer_new()`](./builtins/misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`constant()`](./builtins/misc/constant.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | | [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | | [`defined()`](./builtins/misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | | [`empty()`](./builtins/misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | @@ -369,11 +384,13 @@ sidebar: | [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | | [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | | [`addslashes()`](./builtins/string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | -| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | | [`base64_encode()`](./builtins/string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`bin2hex()`](./builtins/string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`chop()`](./builtins/string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | | [`chr()`](./builtins/string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`chunk_split()`](./builtins/string/chunk_split.md) | `(string $string, int $length = 76, string $separator = '\r\n'): string` | `string` | ✓ | ✓ | +| [`count_chars()`](./builtins/string/count_chars.md) | `(string $string, int $mode = 0): array|string` | `array|string` | ✓ | ✓ | | [`crc32()`](./builtins/string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | | [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | | [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | @@ -397,6 +414,7 @@ sidebar: | [`inet_ntop()`](./builtins/string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | | [`inet_pton()`](./builtins/string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | | [`ip2long()`](./builtins/string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`join()`](./builtins/string/join.md) | `(mixed $separator, mixed $array = null): string` | `string` | ✓ | — | | [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | @@ -407,6 +425,8 @@ sidebar: | [`ord()`](./builtins/string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | | [`parse_url()`](./builtins/string/parse_url.md) | `(string $url, int $component = -1): mixed` | `mixed` | ✓ | ✓ | | [`printf()`](./builtins/string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`quoted_printable_encode()`](./builtins/string/quoted_printable_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`quotemeta()`](./builtins/string/quotemeta.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rawurldecode()`](./builtins/string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rawurlencode()`](./builtins/string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rtrim()`](./builtins/string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | @@ -421,17 +441,24 @@ sidebar: | [`str_replace()`](./builtins/string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | | [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | | [`str_starts_with()`](./builtins/string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_word_count()`](./builtins/string/str_word_count.md) | `(string $string, int $format = 0, string $characters = null): array|int` | `array|int` | ✓ | ✓ | | [`strcasecmp()`](./builtins/string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | | [`strcmp()`](./builtins/string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripos()`](./builtins/string/stripos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`stripslashes()`](./builtins/string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`strlen()`](./builtins/string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strncasecmp()`](./builtins/string/strncasecmp.md) | `(string $string1, string $string2, int $length): int` | `int` | ✓ | — | +| [`strncmp()`](./builtins/string/strncmp.md) | `(string $string1, string $string2, int $length): int` | `int` | ✓ | — | | [`strpos()`](./builtins/string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strrev()`](./builtins/string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strripos()`](./builtins/string/strripos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strrpos()`](./builtins/string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): mixed` | `mixed` | ✓ | ✓ | | [`strtolower()`](./builtins/string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`strtoupper()`](./builtins/string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtr()`](./builtins/string/strtr.md) | `(string $string, array|string $from, string $to = null): string` | `string` | ✓ | ✓ | | [`substr()`](./builtins/string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_count()`](./builtins/string/substr_count.md) | `(string $haystack, string $needle, int $offset = 0, mixed $length = null): int` | `int` | ✓ | — | | [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | | [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | | [`ucfirst()`](./builtins/string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | @@ -450,7 +477,7 @@ sidebar: | [`get_resource_id()`](./builtins/type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | | [`get_resource_type()`](./builtins/type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | | [`gettype()`](./builtins/type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | -| [`intval()`](./builtins/type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`intval()`](./builtins/type/intval.md) | `(mixed $value, int $base = 10): int` | `int` | ✓ | ✓ | | [`is_array()`](./builtins/type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_bool()`](./builtins/type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_callable()`](./builtins/type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index 3cee753978..8931f3c74c 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -11,9 +11,10 @@ sidebar: |---|---|---|:-:|:-:| | [`array_all()`](./array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | | [`array_any()`](./array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | -| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | | [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | | [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_count_values()`](./array/array_count_values.md) | `(array $array): array` | `array` | ✓ | ✓ | | [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | | [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | | [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | @@ -42,11 +43,11 @@ sidebar: | [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | | [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | | [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | -| [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_reverse()`](./array/array_reverse.md) | `(array $array, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | | [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | | [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | -| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | -| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null, bool $preserve_keys = false): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null, array $replacement = []): array` | `array` | ✓ | ✓ | | [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | | [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | | [`array_uintersect()`](./array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | @@ -60,12 +61,18 @@ sidebar: | [`call_user_func()`](./array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | | [`call_user_func_array()`](./array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | | [`count()`](./array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`current()`](./array/current.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`end()`](./array/end.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`key()`](./array/key.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`krsort()`](./array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`ksort()`](./array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`natcasesort()`](./array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`natsort()`](./array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | -| [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`next()`](./array/next.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`prev()`](./array/prev.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`range()`](./array/range.md) | `(mixed $start, mixed $end, int $step = 1): array` | `array` | ✓ | ✓ | +| [`reset()`](./array/reset.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | | [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | | [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array/array_chunk.md b/docs/php/builtins/array/array_chunk.md index be26d0ee5d..c961fbfa28 100644 --- a/docs/php/builtins/array/array_chunk.md +++ b/docs/php/builtins/array/array_chunk.md @@ -8,7 +8,7 @@ sidebar: ## array_chunk() ```php -function array_chunk(array $array, int $length): array +function array_chunk(array $array, int $length, bool $preserve_keys = false): array ``` Splits an array into chunks of the given size. @@ -16,6 +16,7 @@ Splits an array into chunks of the given size. **Parameters**: - `$array` (`array`) - `$length` (`int`) +- `$preserve_keys` (`bool`), default `false`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_count_values.md b/docs/php/builtins/array/array_count_values.md new file mode 100644 index 0000000000..ad355535b7 --- /dev/null +++ b/docs/php/builtins/array/array_count_values.md @@ -0,0 +1,36 @@ +--- +title: "array_count_values()" +description: "Counts the occurrences of each distinct value in an array." +sidebar: + order: 6 +--- + +## array_count_values() + +```php +function array_count_values(array $array): array +``` + +Counts the occurrences of each distinct value in an array. + +**Parameters**: +- `$array` (`array`) + +**Returns**: `array` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `array_count_values` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/array_count_values.md). diff --git a/docs/php/builtins/array/array_diff.md b/docs/php/builtins/array/array_diff.md index 7b39321d5a..99da4fce75 100644 --- a/docs/php/builtins/array/array_diff.md +++ b/docs/php/builtins/array/array_diff.md @@ -2,7 +2,7 @@ title: "array_diff()" description: "Computes the difference of arrays." sidebar: - order: 6 + order: 7 --- ## array_diff() diff --git a/docs/php/builtins/array/array_diff_assoc.md b/docs/php/builtins/array/array_diff_assoc.md index cc343fe8c2..ae1f05de69 100644 --- a/docs/php/builtins/array/array_diff_assoc.md +++ b/docs/php/builtins/array/array_diff_assoc.md @@ -2,7 +2,7 @@ title: "array_diff_assoc()" description: "Computes the difference of arrays with additional index check." sidebar: - order: 7 + order: 8 --- ## array_diff_assoc() diff --git a/docs/php/builtins/array/array_diff_key.md b/docs/php/builtins/array/array_diff_key.md index 149c83eb8b..bfc526e315 100644 --- a/docs/php/builtins/array/array_diff_key.md +++ b/docs/php/builtins/array/array_diff_key.md @@ -2,7 +2,7 @@ title: "array_diff_key()" description: "Computes the difference of arrays using keys for comparison." sidebar: - order: 8 + order: 9 --- ## array_diff_key() diff --git a/docs/php/builtins/array/array_fill.md b/docs/php/builtins/array/array_fill.md index 785dd4453c..d0f8374311 100644 --- a/docs/php/builtins/array/array_fill.md +++ b/docs/php/builtins/array/array_fill.md @@ -2,7 +2,7 @@ title: "array_fill()" description: "Fill an array with values." sidebar: - order: 9 + order: 10 --- ## array_fill() diff --git a/docs/php/builtins/array/array_fill_keys.md b/docs/php/builtins/array/array_fill_keys.md index 89eae5bfdd..fea8e2a2fc 100644 --- a/docs/php/builtins/array/array_fill_keys.md +++ b/docs/php/builtins/array/array_fill_keys.md @@ -2,7 +2,7 @@ title: "array_fill_keys()" description: "Fill an array with values, specifying keys." sidebar: - order: 10 + order: 11 --- ## array_fill_keys() diff --git a/docs/php/builtins/array/array_filter.md b/docs/php/builtins/array/array_filter.md index ce5b566d74..343387a5c2 100644 --- a/docs/php/builtins/array/array_filter.md +++ b/docs/php/builtins/array/array_filter.md @@ -2,7 +2,7 @@ title: "array_filter()" description: "Filters elements of an array using a callback function." sidebar: - order: 11 + order: 12 --- ## array_filter() diff --git a/docs/php/builtins/array/array_find.md b/docs/php/builtins/array/array_find.md index 55f17e9abd..8ad2b76cb5 100644 --- a/docs/php/builtins/array/array_find.md +++ b/docs/php/builtins/array/array_find.md @@ -2,7 +2,7 @@ title: "array_find()" description: "Returns the first element satisfying a predicate callback, or null." sidebar: - order: 12 + order: 13 --- ## array_find() diff --git a/docs/php/builtins/array/array_flip.md b/docs/php/builtins/array/array_flip.md index 7e494a231d..a70639c4c1 100644 --- a/docs/php/builtins/array/array_flip.md +++ b/docs/php/builtins/array/array_flip.md @@ -2,7 +2,7 @@ title: "array_flip()" description: "Exchanges all keys with their associated values in an array." sidebar: - order: 13 + order: 14 --- ## array_flip() diff --git a/docs/php/builtins/array/array_intersect.md b/docs/php/builtins/array/array_intersect.md index ff432c1e5a..98efbc55cd 100644 --- a/docs/php/builtins/array/array_intersect.md +++ b/docs/php/builtins/array/array_intersect.md @@ -2,7 +2,7 @@ title: "array_intersect()" description: "Computes the intersection of arrays." sidebar: - order: 14 + order: 15 --- ## array_intersect() diff --git a/docs/php/builtins/array/array_intersect_assoc.md b/docs/php/builtins/array/array_intersect_assoc.md index d9e3d32ef4..94716ed65b 100644 --- a/docs/php/builtins/array/array_intersect_assoc.md +++ b/docs/php/builtins/array/array_intersect_assoc.md @@ -2,7 +2,7 @@ title: "array_intersect_assoc()" description: "Computes the intersection of arrays with additional index check." sidebar: - order: 15 + order: 16 --- ## array_intersect_assoc() diff --git a/docs/php/builtins/array/array_intersect_key.md b/docs/php/builtins/array/array_intersect_key.md index 5c2c9e5f54..f3c93bfde7 100644 --- a/docs/php/builtins/array/array_intersect_key.md +++ b/docs/php/builtins/array/array_intersect_key.md @@ -2,7 +2,7 @@ title: "array_intersect_key()" description: "Computes the intersection of arrays using keys for comparison." sidebar: - order: 16 + order: 17 --- ## array_intersect_key() diff --git a/docs/php/builtins/array/array_is_list.md b/docs/php/builtins/array/array_is_list.md index e2248369e6..401d7351cc 100644 --- a/docs/php/builtins/array/array_is_list.md +++ b/docs/php/builtins/array/array_is_list.md @@ -2,7 +2,7 @@ title: "array_is_list()" description: "Checks whether an array is a list (sequential 0-based integer keys)." sidebar: - order: 17 + order: 18 --- ## array_is_list() diff --git a/docs/php/builtins/array/array_key_exists.md b/docs/php/builtins/array/array_key_exists.md index 07c67f4867..1f2ced4c5f 100644 --- a/docs/php/builtins/array/array_key_exists.md +++ b/docs/php/builtins/array/array_key_exists.md @@ -2,7 +2,7 @@ title: "array_key_exists()" description: "Checks if the given key or index exists in the array." sidebar: - order: 18 + order: 19 --- ## array_key_exists() diff --git a/docs/php/builtins/array/array_key_first.md b/docs/php/builtins/array/array_key_first.md index d875c3ee5c..e8968b5a2f 100644 --- a/docs/php/builtins/array/array_key_first.md +++ b/docs/php/builtins/array/array_key_first.md @@ -2,7 +2,7 @@ title: "array_key_first()" description: "Gets the first key of an array." sidebar: - order: 19 + order: 20 --- ## array_key_first() diff --git a/docs/php/builtins/array/array_key_last.md b/docs/php/builtins/array/array_key_last.md index e942f46831..9708283240 100644 --- a/docs/php/builtins/array/array_key_last.md +++ b/docs/php/builtins/array/array_key_last.md @@ -2,7 +2,7 @@ title: "array_key_last()" description: "Gets the last key of an array." sidebar: - order: 20 + order: 21 --- ## array_key_last() diff --git a/docs/php/builtins/array/array_keys.md b/docs/php/builtins/array/array_keys.md index 3a81773d92..e1ebb76744 100644 --- a/docs/php/builtins/array/array_keys.md +++ b/docs/php/builtins/array/array_keys.md @@ -2,7 +2,7 @@ title: "array_keys()" description: "Returns all the keys of an array." sidebar: - order: 21 + order: 22 --- ## array_keys() diff --git a/docs/php/builtins/array/array_map.md b/docs/php/builtins/array/array_map.md index eecc4266a9..75c838cd73 100644 --- a/docs/php/builtins/array/array_map.md +++ b/docs/php/builtins/array/array_map.md @@ -2,7 +2,7 @@ title: "array_map()" description: "Applies a callback to the elements of an array." sidebar: - order: 22 + order: 23 --- ## array_map() diff --git a/docs/php/builtins/array/array_merge.md b/docs/php/builtins/array/array_merge.md index 2fe1b6ea8c..95957df346 100644 --- a/docs/php/builtins/array/array_merge.md +++ b/docs/php/builtins/array/array_merge.md @@ -2,7 +2,7 @@ title: "array_merge()" description: "Merges the elements of two arrays." sidebar: - order: 23 + order: 24 --- ## array_merge() diff --git a/docs/php/builtins/array/array_merge_recursive.md b/docs/php/builtins/array/array_merge_recursive.md index 56f45fd8fb..974128229c 100644 --- a/docs/php/builtins/array/array_merge_recursive.md +++ b/docs/php/builtins/array/array_merge_recursive.md @@ -2,7 +2,7 @@ title: "array_merge_recursive()" description: "Recursively merges two arrays, combining scalar collisions into lists." sidebar: - order: 24 + order: 25 --- ## array_merge_recursive() diff --git a/docs/php/builtins/array/array_multisort.md b/docs/php/builtins/array/array_multisort.md index 57e77e53ef..467193c001 100644 --- a/docs/php/builtins/array/array_multisort.md +++ b/docs/php/builtins/array/array_multisort.md @@ -2,7 +2,7 @@ title: "array_multisort()" description: "Sorts multiple arrays or multi-dimensional arrays." sidebar: - order: 25 + order: 26 --- ## array_multisort() diff --git a/docs/php/builtins/array/array_pad.md b/docs/php/builtins/array/array_pad.md index 17351987fb..f0a2e57c05 100644 --- a/docs/php/builtins/array/array_pad.md +++ b/docs/php/builtins/array/array_pad.md @@ -2,7 +2,7 @@ title: "array_pad()" description: "Pads an array to the specified length with a value." sidebar: - order: 26 + order: 27 --- ## array_pad() diff --git a/docs/php/builtins/array/array_pop.md b/docs/php/builtins/array/array_pop.md index 8c978dda86..43d9a33aa8 100644 --- a/docs/php/builtins/array/array_pop.md +++ b/docs/php/builtins/array/array_pop.md @@ -2,7 +2,7 @@ title: "array_pop()" description: "Pops the element off the end of array." sidebar: - order: 27 + order: 28 --- ## array_pop() diff --git a/docs/php/builtins/array/array_product.md b/docs/php/builtins/array/array_product.md index 4df2c7174e..0bf51a4d5a 100644 --- a/docs/php/builtins/array/array_product.md +++ b/docs/php/builtins/array/array_product.md @@ -2,7 +2,7 @@ title: "array_product()" description: "Calculate the product of values in an array." sidebar: - order: 28 + order: 29 --- ## array_product() diff --git a/docs/php/builtins/array/array_push.md b/docs/php/builtins/array/array_push.md index ff36fccdf4..1084684e87 100644 --- a/docs/php/builtins/array/array_push.md +++ b/docs/php/builtins/array/array_push.md @@ -2,7 +2,7 @@ title: "array_push()" description: "Pushes one or more elements onto the end of array." sidebar: - order: 29 + order: 30 --- ## array_push() diff --git a/docs/php/builtins/array/array_rand.md b/docs/php/builtins/array/array_rand.md index b27fe9d94e..0bb46d999c 100644 --- a/docs/php/builtins/array/array_rand.md +++ b/docs/php/builtins/array/array_rand.md @@ -2,7 +2,7 @@ title: "array_rand()" description: "Pick one or more random keys out of an array." sidebar: - order: 30 + order: 31 --- ## array_rand() diff --git a/docs/php/builtins/array/array_reduce.md b/docs/php/builtins/array/array_reduce.md index 85427785fc..28e8f09672 100644 --- a/docs/php/builtins/array/array_reduce.md +++ b/docs/php/builtins/array/array_reduce.md @@ -2,7 +2,7 @@ title: "array_reduce()" description: "Iteratively reduces an array to a single value using a callback function." sidebar: - order: 31 + order: 32 --- ## array_reduce() diff --git a/docs/php/builtins/array/array_replace.md b/docs/php/builtins/array/array_replace.md index 62f1f8ee43..27dc613e5b 100644 --- a/docs/php/builtins/array/array_replace.md +++ b/docs/php/builtins/array/array_replace.md @@ -2,7 +2,7 @@ title: "array_replace()" description: "Replaces elements from passed arrays into the first array." sidebar: - order: 32 + order: 33 --- ## array_replace() diff --git a/docs/php/builtins/array/array_replace_recursive.md b/docs/php/builtins/array/array_replace_recursive.md index 709730609c..255c378ea2 100644 --- a/docs/php/builtins/array/array_replace_recursive.md +++ b/docs/php/builtins/array/array_replace_recursive.md @@ -2,7 +2,7 @@ title: "array_replace_recursive()" description: "Replaces elements from passed arrays into the first array recursively." sidebar: - order: 33 + order: 34 --- ## array_replace_recursive() diff --git a/docs/php/builtins/array/array_reverse.md b/docs/php/builtins/array/array_reverse.md index 48223a9088..9159f33125 100644 --- a/docs/php/builtins/array/array_reverse.md +++ b/docs/php/builtins/array/array_reverse.md @@ -2,19 +2,20 @@ title: "array_reverse()" description: "Returns an array with the elements in reverse order." sidebar: - order: 34 + order: 35 --- ## array_reverse() ```php -function array_reverse(array $array): array +function array_reverse(array $array, bool $preserve_keys = false): array ``` Returns an array with the elements in reverse order. **Parameters**: - `$array` (`array`) +- `$preserve_keys` (`bool`), default `false`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_search.md b/docs/php/builtins/array/array_search.md index 8dd645824c..f2e795073d 100644 --- a/docs/php/builtins/array/array_search.md +++ b/docs/php/builtins/array/array_search.md @@ -2,7 +2,7 @@ title: "array_search()" description: "Searches the array for a given value and returns the first corresponding key if successful." sidebar: - order: 35 + order: 36 --- ## array_search() diff --git a/docs/php/builtins/array/array_shift.md b/docs/php/builtins/array/array_shift.md index 4e2f14ad45..d004e0c568 100644 --- a/docs/php/builtins/array/array_shift.md +++ b/docs/php/builtins/array/array_shift.md @@ -2,7 +2,7 @@ title: "array_shift()" description: "Shifts an element off the beginning of array." sidebar: - order: 36 + order: 37 --- ## array_shift() diff --git a/docs/php/builtins/array/array_slice.md b/docs/php/builtins/array/array_slice.md index 8f8ab46463..06c0291733 100644 --- a/docs/php/builtins/array/array_slice.md +++ b/docs/php/builtins/array/array_slice.md @@ -2,13 +2,13 @@ title: "array_slice()" description: "Extracts a slice of an array." sidebar: - order: 37 + order: 38 --- ## array_slice() ```php -function array_slice(array $array, int $offset, int $length = null): array +function array_slice(array $array, int $offset, int $length = null, bool $preserve_keys = false): array ``` Extracts a slice of an array. @@ -17,6 +17,7 @@ Extracts a slice of an array. - `$array` (`array`) - `$offset` (`int`) - `$length` (`int`), default `null`, optional +- `$preserve_keys` (`bool`), default `false`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_splice.md b/docs/php/builtins/array/array_splice.md index 4cc6355c91..25e115f382 100644 --- a/docs/php/builtins/array/array_splice.md +++ b/docs/php/builtins/array/array_splice.md @@ -2,13 +2,13 @@ title: "array_splice()" description: "Removes a portion of the array and replaces it with something else." sidebar: - order: 38 + order: 39 --- ## array_splice() ```php -function array_splice(array $array, int $offset, int $length = null): array +function array_splice(array $array, int $offset, int $length = null, array $replacement = []): array ``` Removes a portion of the array and replaces it with something else. @@ -17,6 +17,7 @@ Removes a portion of the array and replaces it with something else. - `$array` (`array`), passed by reference - `$offset` (`int`) - `$length` (`int`), default `null`, optional +- `$replacement` (`array`), default `[]`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/array_sum.md b/docs/php/builtins/array/array_sum.md index 92d135d247..964dc461a3 100644 --- a/docs/php/builtins/array/array_sum.md +++ b/docs/php/builtins/array/array_sum.md @@ -2,7 +2,7 @@ title: "array_sum()" description: "Calculate the sum of values in an array." sidebar: - order: 39 + order: 40 --- ## array_sum() diff --git a/docs/php/builtins/array/array_udiff.md b/docs/php/builtins/array/array_udiff.md index 666d4b5d8f..9101b9c5bc 100644 --- a/docs/php/builtins/array/array_udiff.md +++ b/docs/php/builtins/array/array_udiff.md @@ -2,7 +2,7 @@ title: "array_udiff()" description: "Computes the difference of arrays using a callback comparator." sidebar: - order: 40 + order: 41 --- ## array_udiff() diff --git a/docs/php/builtins/array/array_uintersect.md b/docs/php/builtins/array/array_uintersect.md index e1d801c6d7..d373730db8 100644 --- a/docs/php/builtins/array/array_uintersect.md +++ b/docs/php/builtins/array/array_uintersect.md @@ -2,7 +2,7 @@ title: "array_uintersect()" description: "Computes the intersection of arrays using a callback comparator." sidebar: - order: 41 + order: 42 --- ## array_uintersect() diff --git a/docs/php/builtins/array/array_unique.md b/docs/php/builtins/array/array_unique.md index 8e202cf5af..b9cbbf7476 100644 --- a/docs/php/builtins/array/array_unique.md +++ b/docs/php/builtins/array/array_unique.md @@ -2,7 +2,7 @@ title: "array_unique()" description: "Removes duplicate values from an array." sidebar: - order: 42 + order: 43 --- ## array_unique() diff --git a/docs/php/builtins/array/array_unshift.md b/docs/php/builtins/array/array_unshift.md index 5c1cec2686..aa7b34d095 100644 --- a/docs/php/builtins/array/array_unshift.md +++ b/docs/php/builtins/array/array_unshift.md @@ -2,7 +2,7 @@ title: "array_unshift()" description: "Prepends one or more elements to the beginning of an array." sidebar: - order: 43 + order: 44 --- ## array_unshift() diff --git a/docs/php/builtins/array/array_values.md b/docs/php/builtins/array/array_values.md index 0fbe78381c..c55ec667ff 100644 --- a/docs/php/builtins/array/array_values.md +++ b/docs/php/builtins/array/array_values.md @@ -2,7 +2,7 @@ title: "array_values()" description: "Returns all the values of an array, re-indexed numerically." sidebar: - order: 44 + order: 45 --- ## array_values() diff --git a/docs/php/builtins/array/array_walk.md b/docs/php/builtins/array/array_walk.md index 522c630d5c..29a8e8ea2e 100644 --- a/docs/php/builtins/array/array_walk.md +++ b/docs/php/builtins/array/array_walk.md @@ -2,7 +2,7 @@ title: "array_walk()" description: "Applies a user function to every member of an array." sidebar: - order: 45 + order: 46 --- ## array_walk() diff --git a/docs/php/builtins/array/array_walk_recursive.md b/docs/php/builtins/array/array_walk_recursive.md index a2b37de046..518aefd661 100644 --- a/docs/php/builtins/array/array_walk_recursive.md +++ b/docs/php/builtins/array/array_walk_recursive.md @@ -2,7 +2,7 @@ title: "array_walk_recursive()" description: "Applies a user function recursively to every member of an array." sidebar: - order: 46 + order: 47 --- ## array_walk_recursive() diff --git a/docs/php/builtins/array/arsort.md b/docs/php/builtins/array/arsort.md index 172ce60903..be139da659 100644 --- a/docs/php/builtins/array/arsort.md +++ b/docs/php/builtins/array/arsort.md @@ -2,7 +2,7 @@ title: "arsort()" description: "Sorts an array in descending order and maintains index association." sidebar: - order: 47 + order: 48 --- ## arsort() diff --git a/docs/php/builtins/array/asort.md b/docs/php/builtins/array/asort.md index 209d5eaf0f..2eb532475d 100644 --- a/docs/php/builtins/array/asort.md +++ b/docs/php/builtins/array/asort.md @@ -2,7 +2,7 @@ title: "asort()" description: "Sorts an array and maintains index association." sidebar: - order: 48 + order: 49 --- ## asort() diff --git a/docs/php/builtins/array/call_user_func.md b/docs/php/builtins/array/call_user_func.md index 760a7c1df8..2103b459c1 100644 --- a/docs/php/builtins/array/call_user_func.md +++ b/docs/php/builtins/array/call_user_func.md @@ -2,7 +2,7 @@ title: "call_user_func()" description: "Calls a callback with the given arguments." sidebar: - order: 49 + order: 50 --- ## call_user_func() diff --git a/docs/php/builtins/array/call_user_func_array.md b/docs/php/builtins/array/call_user_func_array.md index b544354406..dec9b9d8f4 100644 --- a/docs/php/builtins/array/call_user_func_array.md +++ b/docs/php/builtins/array/call_user_func_array.md @@ -2,7 +2,7 @@ title: "call_user_func_array()" description: "Calls a callback with an array of parameters." sidebar: - order: 50 + order: 51 --- ## call_user_func_array() diff --git a/docs/php/builtins/array/count.md b/docs/php/builtins/array/count.md index 8c134b740e..7ec905ab67 100644 --- a/docs/php/builtins/array/count.md +++ b/docs/php/builtins/array/count.md @@ -2,7 +2,7 @@ title: "count()" description: "Counts all elements in an array or Countable object." sidebar: - order: 51 + order: 52 --- ## count() diff --git a/docs/php/builtins/array/current.md b/docs/php/builtins/array/current.md new file mode 100644 index 0000000000..13ad8e121a --- /dev/null +++ b/docs/php/builtins/array/current.md @@ -0,0 +1,36 @@ +--- +title: "current()" +description: "Returns the element under the array's internal pointer." +sidebar: + order: 53 +--- + +## current() + +```php +function current(array $array): mixed +``` + +Returns the element under the array's internal pointer. + +**Parameters**: +- `$array` (`array`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/current.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/current.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `current` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/current.md). diff --git a/docs/php/builtins/array/end.md b/docs/php/builtins/array/end.md new file mode 100644 index 0000000000..2f88436f75 --- /dev/null +++ b/docs/php/builtins/array/end.md @@ -0,0 +1,36 @@ +--- +title: "end()" +description: "Moves the array's internal pointer to the last element and returns it." +sidebar: + order: 54 +--- + +## end() + +```php +function end(array $array): mixed +``` + +Moves the array's internal pointer to the last element and returns it. + +**Parameters**: +- `$array` (`array`), passed by reference + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/end.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/end.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `end` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/end.md). diff --git a/docs/php/builtins/array/in_array.md b/docs/php/builtins/array/in_array.md index 88418c680c..a7c32585a3 100644 --- a/docs/php/builtins/array/in_array.md +++ b/docs/php/builtins/array/in_array.md @@ -2,7 +2,7 @@ title: "in_array()" description: "Checks if a value exists in an array." sidebar: - order: 52 + order: 55 --- ## in_array() diff --git a/docs/php/builtins/array/key.md b/docs/php/builtins/array/key.md new file mode 100644 index 0000000000..886c18db11 --- /dev/null +++ b/docs/php/builtins/array/key.md @@ -0,0 +1,36 @@ +--- +title: "key()" +description: "Returns the key of the element under the array's internal pointer." +sidebar: + order: 56 +--- + +## key() + +```php +function key(array $array): mixed +``` + +Returns the key of the element under the array's internal pointer. + +**Parameters**: +- `$array` (`array`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/key.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `key` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/key.md). diff --git a/docs/php/builtins/array/krsort.md b/docs/php/builtins/array/krsort.md index 119620599d..c2e517931c 100644 --- a/docs/php/builtins/array/krsort.md +++ b/docs/php/builtins/array/krsort.md @@ -2,7 +2,7 @@ title: "krsort()" description: "Sorts an array by key in descending order." sidebar: - order: 53 + order: 57 --- ## krsort() diff --git a/docs/php/builtins/array/ksort.md b/docs/php/builtins/array/ksort.md index dea6a01929..74a853de65 100644 --- a/docs/php/builtins/array/ksort.md +++ b/docs/php/builtins/array/ksort.md @@ -2,7 +2,7 @@ title: "ksort()" description: "Sorts an array by key in ascending order." sidebar: - order: 54 + order: 58 --- ## ksort() diff --git a/docs/php/builtins/array/natcasesort.md b/docs/php/builtins/array/natcasesort.md index b679f6c411..94f4ae5853 100644 --- a/docs/php/builtins/array/natcasesort.md +++ b/docs/php/builtins/array/natcasesort.md @@ -2,7 +2,7 @@ title: "natcasesort()" description: "Sorts an array using a case-insensitive natural order algorithm." sidebar: - order: 55 + order: 59 --- ## natcasesort() diff --git a/docs/php/builtins/array/natsort.md b/docs/php/builtins/array/natsort.md index 857c744247..157d9183f9 100644 --- a/docs/php/builtins/array/natsort.md +++ b/docs/php/builtins/array/natsort.md @@ -2,7 +2,7 @@ title: "natsort()" description: "Sorts an array using a natural order algorithm." sidebar: - order: 56 + order: 60 --- ## natsort() diff --git a/docs/php/builtins/array/next.md b/docs/php/builtins/array/next.md new file mode 100644 index 0000000000..29a8945754 --- /dev/null +++ b/docs/php/builtins/array/next.md @@ -0,0 +1,36 @@ +--- +title: "next()" +description: "Advances the array's internal pointer and returns the new element." +sidebar: + order: 61 +--- + +## next() + +```php +function next(array $array): mixed +``` + +Advances the array's internal pointer and returns the new element. + +**Parameters**: +- `$array` (`array`), passed by reference + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/next.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/next.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `next` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/next.md). diff --git a/docs/php/builtins/array/prev.md b/docs/php/builtins/array/prev.md new file mode 100644 index 0000000000..9b47fd0104 --- /dev/null +++ b/docs/php/builtins/array/prev.md @@ -0,0 +1,36 @@ +--- +title: "prev()" +description: "Rewinds the array's internal pointer and returns the new element." +sidebar: + order: 62 +--- + +## prev() + +```php +function prev(array $array): mixed +``` + +Rewinds the array's internal pointer and returns the new element. + +**Parameters**: +- `$array` (`array`), passed by reference + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/prev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/prev.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `prev` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/prev.md). diff --git a/docs/php/builtins/array/range.md b/docs/php/builtins/array/range.md index a4f04fc33a..db8dc39d90 100644 --- a/docs/php/builtins/array/range.md +++ b/docs/php/builtins/array/range.md @@ -2,13 +2,13 @@ title: "range()" description: "Create an array containing a range of elements." sidebar: - order: 57 + order: 63 --- ## range() ```php -function range(mixed $start, mixed $end): array +function range(mixed $start, mixed $end, int $step = 1): array ``` Create an array containing a range of elements. @@ -16,6 +16,7 @@ Create an array containing a range of elements. **Parameters**: - `$start` (`mixed`) - `$end` (`mixed`) +- `$step` (`int`), default `1`, optional **Returns**: `array` diff --git a/docs/php/builtins/array/reset.md b/docs/php/builtins/array/reset.md new file mode 100644 index 0000000000..c0357d86b6 --- /dev/null +++ b/docs/php/builtins/array/reset.md @@ -0,0 +1,36 @@ +--- +title: "reset()" +description: "Rewinds the array's internal pointer to the first element and returns it." +sidebar: + order: 64 +--- + +## reset() + +```php +function reset(array $array): mixed +``` + +Rewinds the array's internal pointer to the first element and returns it. + +**Parameters**: +- `$array` (`array`), passed by reference + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/reset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/reset.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `reset` is implemented in the compiler, see [the internals page](../../../internals/builtins/array/reset.md). diff --git a/docs/php/builtins/array/rsort.md b/docs/php/builtins/array/rsort.md index 27506824b2..c4a3da0c19 100644 --- a/docs/php/builtins/array/rsort.md +++ b/docs/php/builtins/array/rsort.md @@ -2,7 +2,7 @@ title: "rsort()" description: "Sorts an array in descending order." sidebar: - order: 58 + order: 65 --- ## rsort() diff --git a/docs/php/builtins/array/shuffle.md b/docs/php/builtins/array/shuffle.md index 78cd6db0fd..fa978e00d0 100644 --- a/docs/php/builtins/array/shuffle.md +++ b/docs/php/builtins/array/shuffle.md @@ -2,7 +2,7 @@ title: "shuffle()" description: "Shuffles an array into random order." sidebar: - order: 59 + order: 66 --- ## shuffle() diff --git a/docs/php/builtins/array/sort.md b/docs/php/builtins/array/sort.md index 1f5b022a0c..5761930b28 100644 --- a/docs/php/builtins/array/sort.md +++ b/docs/php/builtins/array/sort.md @@ -2,7 +2,7 @@ title: "sort()" description: "Sorts an array in ascending order." sidebar: - order: 60 + order: 67 --- ## sort() diff --git a/docs/php/builtins/array/uasort.md b/docs/php/builtins/array/uasort.md index a9cc2a15d5..769b70fdef 100644 --- a/docs/php/builtins/array/uasort.md +++ b/docs/php/builtins/array/uasort.md @@ -2,7 +2,7 @@ title: "uasort()" description: "Sorts an array with a user-defined comparison function and maintains index association." sidebar: - order: 61 + order: 68 --- ## uasort() diff --git a/docs/php/builtins/array/uksort.md b/docs/php/builtins/array/uksort.md index be0a93da3e..4379dc88b0 100644 --- a/docs/php/builtins/array/uksort.md +++ b/docs/php/builtins/array/uksort.md @@ -2,7 +2,7 @@ title: "uksort()" description: "Sorts an array by keys using a user-defined comparison function." sidebar: - order: 62 + order: 69 --- ## uksort() diff --git a/docs/php/builtins/array/usort.md b/docs/php/builtins/array/usort.md index d227ad0360..f495792c33 100644 --- a/docs/php/builtins/array/usort.md +++ b/docs/php/builtins/array/usort.md @@ -2,7 +2,7 @@ title: "usort()" description: "Sorts an array by values using a user-defined comparison function." sidebar: - order: 63 + order: 70 --- ## usort() diff --git a/docs/php/builtins/buffer/buffer_free.md b/docs/php/builtins/buffer/buffer_free.md index c83772ee85..d77f3a8e8d 100644 --- a/docs/php/builtins/buffer/buffer_free.md +++ b/docs/php/builtins/buffer/buffer_free.md @@ -2,7 +2,7 @@ title: "buffer_free()" description: "Frees a buffer and nulls the local variable that held it." sidebar: - order: 64 + order: 71 --- ## buffer_free() diff --git a/docs/php/builtins/buffer/buffer_len.md b/docs/php/builtins/buffer/buffer_len.md index ab8acb28fe..040d777314 100644 --- a/docs/php/builtins/buffer/buffer_len.md +++ b/docs/php/builtins/buffer/buffer_len.md @@ -2,7 +2,7 @@ title: "buffer_len()" description: "Returns the logical element count of a buffer." sidebar: - order: 65 + order: 72 --- ## buffer_len() diff --git a/docs/php/builtins/class/class_alias.md b/docs/php/builtins/class/class_alias.md index d5c55eac88..fd05ef33ce 100644 --- a/docs/php/builtins/class/class_alias.md +++ b/docs/php/builtins/class/class_alias.md @@ -2,7 +2,7 @@ title: "class_alias()" description: "Creates an alias for a class." sidebar: - order: 66 + order: 73 --- ## class_alias() diff --git a/docs/php/builtins/class/class_attribute_args.md b/docs/php/builtins/class/class_attribute_args.md index 3045916956..d8b3e1bfdb 100644 --- a/docs/php/builtins/class/class_attribute_args.md +++ b/docs/php/builtins/class/class_attribute_args.md @@ -2,7 +2,7 @@ title: "class_attribute_args()" description: "Returns the constructor arguments of a named attribute applied to a class." sidebar: - order: 67 + order: 74 --- ## class_attribute_args() diff --git a/docs/php/builtins/class/class_attribute_names.md b/docs/php/builtins/class/class_attribute_names.md index dc33cbf82a..ca281960f7 100644 --- a/docs/php/builtins/class/class_attribute_names.md +++ b/docs/php/builtins/class/class_attribute_names.md @@ -2,7 +2,7 @@ title: "class_attribute_names()" description: "Returns the list of attribute names applied to a class." sidebar: - order: 68 + order: 75 --- ## class_attribute_names() diff --git a/docs/php/builtins/class/class_exists.md b/docs/php/builtins/class/class_exists.md index 30358bb5c8..33f49bd5b6 100644 --- a/docs/php/builtins/class/class_exists.md +++ b/docs/php/builtins/class/class_exists.md @@ -2,7 +2,7 @@ title: "class_exists()" description: "Checks whether the given class has been defined." sidebar: - order: 69 + order: 76 --- ## class_exists() diff --git a/docs/php/builtins/class/class_get_attributes.md b/docs/php/builtins/class/class_get_attributes.md index 5961cb596f..49b1412599 100644 --- a/docs/php/builtins/class/class_get_attributes.md +++ b/docs/php/builtins/class/class_get_attributes.md @@ -2,7 +2,7 @@ title: "class_get_attributes()" description: "Returns an array of ReflectionAttribute objects for all attributes of a class." sidebar: - order: 70 + order: 77 --- ## class_get_attributes() diff --git a/docs/php/builtins/class/class_implements.md b/docs/php/builtins/class/class_implements.md index 1850806c20..17687d0b4c 100644 --- a/docs/php/builtins/class/class_implements.md +++ b/docs/php/builtins/class/class_implements.md @@ -2,7 +2,7 @@ title: "class_implements()" description: "Returns the interfaces which are implemented by the given class or its parents." sidebar: - order: 71 + order: 78 --- ## class_implements() diff --git a/docs/php/builtins/class/class_parents.md b/docs/php/builtins/class/class_parents.md index 791b74da6d..a87e1ec159 100644 --- a/docs/php/builtins/class/class_parents.md +++ b/docs/php/builtins/class/class_parents.md @@ -2,7 +2,7 @@ title: "class_parents()" description: "Returns the parent classes of the given class." sidebar: - order: 72 + order: 79 --- ## class_parents() diff --git a/docs/php/builtins/class/class_uses.md b/docs/php/builtins/class/class_uses.md index 728f68943f..bc4a365e27 100644 --- a/docs/php/builtins/class/class_uses.md +++ b/docs/php/builtins/class/class_uses.md @@ -2,7 +2,7 @@ title: "class_uses()" description: "Returns the traits used by the given class." sidebar: - order: 73 + order: 80 --- ## class_uses() diff --git a/docs/php/builtins/class/enum_exists.md b/docs/php/builtins/class/enum_exists.md index 6083316cbf..bc9bd2a124 100644 --- a/docs/php/builtins/class/enum_exists.md +++ b/docs/php/builtins/class/enum_exists.md @@ -2,7 +2,7 @@ title: "enum_exists()" description: "Checks if the enum has been defined." sidebar: - order: 74 + order: 81 --- ## enum_exists() diff --git a/docs/php/builtins/class/function_exists.md b/docs/php/builtins/class/function_exists.md index 4a455dd363..a0d0f11ede 100644 --- a/docs/php/builtins/class/function_exists.md +++ b/docs/php/builtins/class/function_exists.md @@ -2,7 +2,7 @@ title: "function_exists()" description: "Returns true if the given function has been defined." sidebar: - order: 75 + order: 82 --- ## function_exists() diff --git a/docs/php/builtins/class/get_called_class.md b/docs/php/builtins/class/get_called_class.md index 15b96f8633..711d28131c 100644 --- a/docs/php/builtins/class/get_called_class.md +++ b/docs/php/builtins/class/get_called_class.md @@ -2,7 +2,7 @@ title: "get_called_class()" description: "get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." sidebar: - order: 76 + order: 83 --- ## get_called_class() diff --git a/docs/php/builtins/class/get_class.md b/docs/php/builtins/class/get_class.md index d00392ee81..53388fea72 100644 --- a/docs/php/builtins/class/get_class.md +++ b/docs/php/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class()" description: "Returns the name of the class of an object." sidebar: - order: 77 + order: 84 --- ## get_class() diff --git a/docs/php/builtins/class/get_class_methods.md b/docs/php/builtins/class/get_class_methods.md index 77d23d0780..5fa4b1037d 100644 --- a/docs/php/builtins/class/get_class_methods.md +++ b/docs/php/builtins/class/get_class_methods.md @@ -2,7 +2,7 @@ title: "get_class_methods()" description: "get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." sidebar: - order: 78 + order: 85 --- ## get_class_methods() diff --git a/docs/php/builtins/class/get_class_vars.md b/docs/php/builtins/class/get_class_vars.md index 2fe580caf0..0a0cb03361 100644 --- a/docs/php/builtins/class/get_class_vars.md +++ b/docs/php/builtins/class/get_class_vars.md @@ -2,7 +2,7 @@ title: "get_class_vars()" description: "get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." sidebar: - order: 79 + order: 86 --- ## get_class_vars() diff --git a/docs/php/builtins/class/get_declared_classes.md b/docs/php/builtins/class/get_declared_classes.md index e7938c9883..940f76c483 100644 --- a/docs/php/builtins/class/get_declared_classes.md +++ b/docs/php/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes()" description: "Returns an array of the names of the defined classes." sidebar: - order: 80 + order: 87 --- ## get_declared_classes() diff --git a/docs/php/builtins/class/get_declared_interfaces.md b/docs/php/builtins/class/get_declared_interfaces.md index f3382e150d..0c2ee2fb77 100644 --- a/docs/php/builtins/class/get_declared_interfaces.md +++ b/docs/php/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces()" description: "Returns an array of all declared interfaces." sidebar: - order: 81 + order: 88 --- ## get_declared_interfaces() diff --git a/docs/php/builtins/class/get_declared_traits.md b/docs/php/builtins/class/get_declared_traits.md index 3a8b4461a9..341603f9bb 100644 --- a/docs/php/builtins/class/get_declared_traits.md +++ b/docs/php/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits()" description: "Returns an array of all declared traits." sidebar: - order: 82 + order: 89 --- ## get_declared_traits() diff --git a/docs/php/builtins/class/get_object_vars.md b/docs/php/builtins/class/get_object_vars.md index 2b4a9ecfc1..2d36ae4181 100644 --- a/docs/php/builtins/class/get_object_vars.md +++ b/docs/php/builtins/class/get_object_vars.md @@ -2,7 +2,7 @@ title: "get_object_vars()" description: "get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." sidebar: - order: 83 + order: 90 --- ## get_object_vars() diff --git a/docs/php/builtins/class/get_parent_class.md b/docs/php/builtins/class/get_parent_class.md index 4ed1d59d82..9d36e59167 100644 --- a/docs/php/builtins/class/get_parent_class.md +++ b/docs/php/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class()" description: "Returns the name of the parent class of an object or class." sidebar: - order: 84 + order: 91 --- ## get_parent_class() diff --git a/docs/php/builtins/class/interface_exists.md b/docs/php/builtins/class/interface_exists.md index 147b140a70..ee238fda75 100644 --- a/docs/php/builtins/class/interface_exists.md +++ b/docs/php/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists()" description: "Checks if the interface has been defined." sidebar: - order: 85 + order: 92 --- ## interface_exists() diff --git a/docs/php/builtins/class/is_a.md b/docs/php/builtins/class/is_a.md index 10cf8980cd..c485525804 100644 --- a/docs/php/builtins/class/is_a.md +++ b/docs/php/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a()" description: "Checks whether an object is of a given type or has it as one of its parents." sidebar: - order: 86 + order: 93 --- ## is_a() diff --git a/docs/php/builtins/class/is_subclass_of.md b/docs/php/builtins/class/is_subclass_of.md index 734365ef03..34876fba9f 100644 --- a/docs/php/builtins/class/is_subclass_of.md +++ b/docs/php/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of()" description: "Checks if the object has a given class as one of its parents or implements it." sidebar: - order: 87 + order: 94 --- ## is_subclass_of() diff --git a/docs/php/builtins/class/method_exists.md b/docs/php/builtins/class/method_exists.md index 56c357c6e6..e156e0813f 100644 --- a/docs/php/builtins/class/method_exists.md +++ b/docs/php/builtins/class/method_exists.md @@ -2,7 +2,7 @@ title: "method_exists()" description: "Checks whether a class method exists." sidebar: - order: 88 + order: 95 --- ## method_exists() diff --git a/docs/php/builtins/class/property_exists.md b/docs/php/builtins/class/property_exists.md index 5c6fa1b8d4..fbe33a67f5 100644 --- a/docs/php/builtins/class/property_exists.md +++ b/docs/php/builtins/class/property_exists.md @@ -2,7 +2,7 @@ title: "property_exists()" description: "Checks whether an object or class has a property." sidebar: - order: 89 + order: 96 --- ## property_exists() diff --git a/docs/php/builtins/class/trait_exists.md b/docs/php/builtins/class/trait_exists.md index 7aec8875e1..c725621b89 100644 --- a/docs/php/builtins/class/trait_exists.md +++ b/docs/php/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists()" description: "Checks whether the trait exists." sidebar: - order: 90 + order: 97 --- ## trait_exists() diff --git a/docs/php/builtins/date/checkdate.md b/docs/php/builtins/date/checkdate.md index 01e0c43261..9ad0a9c801 100644 --- a/docs/php/builtins/date/checkdate.md +++ b/docs/php/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate()" description: "Validates a Gregorian date." sidebar: - order: 91 + order: 98 --- ## checkdate() diff --git a/docs/php/builtins/date/date.md b/docs/php/builtins/date/date.md index 1fa7ac3b6b..ae1b0191f9 100644 --- a/docs/php/builtins/date/date.md +++ b/docs/php/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date()" description: "Formats a local time/date." sidebar: - order: 92 + order: 99 --- ## date() diff --git a/docs/php/builtins/date/date_default_timezone_get.md b/docs/php/builtins/date/date_default_timezone_get.md index 9c13ea406d..921f16931e 100644 --- a/docs/php/builtins/date/date_default_timezone_get.md +++ b/docs/php/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get()" description: "Gets the default timezone." sidebar: - order: 93 + order: 100 --- ## date_default_timezone_get() diff --git a/docs/php/builtins/date/date_default_timezone_set.md b/docs/php/builtins/date/date_default_timezone_set.md index 32ff7e1061..7ffd9f0e85 100644 --- a/docs/php/builtins/date/date_default_timezone_set.md +++ b/docs/php/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set()" description: "Sets the default timezone." sidebar: - order: 94 + order: 101 --- ## date_default_timezone_set() diff --git a/docs/php/builtins/date/getdate.md b/docs/php/builtins/date/getdate.md index 8a27dcd3e5..456985e273 100644 --- a/docs/php/builtins/date/getdate.md +++ b/docs/php/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate()" description: "Returns date/time information." sidebar: - order: 95 + order: 102 --- ## getdate() diff --git a/docs/php/builtins/date/gmdate.md b/docs/php/builtins/date/gmdate.md index e1cccc309b..87e4157ba8 100644 --- a/docs/php/builtins/date/gmdate.md +++ b/docs/php/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate()" description: "Formats a GMT/UTC date and time." sidebar: - order: 96 + order: 103 --- ## gmdate() diff --git a/docs/php/builtins/date/gmmktime.md b/docs/php/builtins/date/gmmktime.md index 10fd1a03aa..e448ca9d5c 100644 --- a/docs/php/builtins/date/gmmktime.md +++ b/docs/php/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime()" description: "Returns the Unix timestamp for a GMT date." sidebar: - order: 97 + order: 104 --- ## gmmktime() diff --git a/docs/php/builtins/date/hrtime.md b/docs/php/builtins/date/hrtime.md index e23d6cabad..74ba600546 100644 --- a/docs/php/builtins/date/hrtime.md +++ b/docs/php/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime()" description: "Returns the current high-resolution time." sidebar: - order: 98 + order: 105 --- ## hrtime() diff --git a/docs/php/builtins/date/localtime.md b/docs/php/builtins/date/localtime.md index 3ea753ee10..0201dd7c63 100644 --- a/docs/php/builtins/date/localtime.md +++ b/docs/php/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime()" description: "Returns the local time." sidebar: - order: 99 + order: 106 --- ## localtime() diff --git a/docs/php/builtins/date/microtime.md b/docs/php/builtins/date/microtime.md index b2b827a6c5..a74e61d3a0 100644 --- a/docs/php/builtins/date/microtime.md +++ b/docs/php/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime()" description: "Returns the current Unix timestamp with microseconds." sidebar: - order: 100 + order: 107 --- ## microtime() diff --git a/docs/php/builtins/date/mktime.md b/docs/php/builtins/date/mktime.md index f2082efd7c..bfe54d8d31 100644 --- a/docs/php/builtins/date/mktime.md +++ b/docs/php/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime()" description: "Returns the Unix timestamp for a date." sidebar: - order: 101 + order: 108 --- ## mktime() diff --git a/docs/php/builtins/date/strtotime.md b/docs/php/builtins/date/strtotime.md index 93791b402d..18cd1cfb55 100644 --- a/docs/php/builtins/date/strtotime.md +++ b/docs/php/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime()" description: "Parses an English textual datetime description into a Unix timestamp." sidebar: - order: 102 + order: 109 --- ## strtotime() diff --git a/docs/php/builtins/date/time.md b/docs/php/builtins/date/time.md index 0b86647249..2824eb2132 100644 --- a/docs/php/builtins/date/time.md +++ b/docs/php/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time()" description: "Returns the current Unix timestamp." sidebar: - order: 103 + order: 110 --- ## time() diff --git a/docs/php/builtins/filesystem/basename.md b/docs/php/builtins/filesystem/basename.md index 18fa4469a9..d4fccfda1c 100644 --- a/docs/php/builtins/filesystem/basename.md +++ b/docs/php/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename()" description: "Returns the trailing name component of a path." sidebar: - order: 104 + order: 111 --- ## basename() diff --git a/docs/php/builtins/filesystem/chdir.md b/docs/php/builtins/filesystem/chdir.md index 7e8c39536f..1277604f1e 100644 --- a/docs/php/builtins/filesystem/chdir.md +++ b/docs/php/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir()" description: "Changes the current directory." sidebar: - order: 105 + order: 112 --- ## chdir() diff --git a/docs/php/builtins/filesystem/chgrp.md b/docs/php/builtins/filesystem/chgrp.md index deb4029355..d100a25acb 100644 --- a/docs/php/builtins/filesystem/chgrp.md +++ b/docs/php/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp()" description: "Changes file group." sidebar: - order: 106 + order: 113 --- ## chgrp() diff --git a/docs/php/builtins/filesystem/chmod.md b/docs/php/builtins/filesystem/chmod.md index 75efe5cae5..76f184527c 100644 --- a/docs/php/builtins/filesystem/chmod.md +++ b/docs/php/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod()" description: "Changes file mode." sidebar: - order: 107 + order: 114 --- ## chmod() diff --git a/docs/php/builtins/filesystem/chown.md b/docs/php/builtins/filesystem/chown.md index 8b57c91612..b89797c157 100644 --- a/docs/php/builtins/filesystem/chown.md +++ b/docs/php/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown()" description: "Changes file owner." sidebar: - order: 108 + order: 115 --- ## chown() diff --git a/docs/php/builtins/filesystem/clearstatcache.md b/docs/php/builtins/filesystem/clearstatcache.md index c12b84241b..975006a20b 100644 --- a/docs/php/builtins/filesystem/clearstatcache.md +++ b/docs/php/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache()" description: "Clears file status cache." sidebar: - order: 109 + order: 116 --- ## clearstatcache() diff --git a/docs/php/builtins/filesystem/copy.md b/docs/php/builtins/filesystem/copy.md index 1555b8c914..76e479f060 100644 --- a/docs/php/builtins/filesystem/copy.md +++ b/docs/php/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy()" description: "Copies a file." sidebar: - order: 110 + order: 117 --- ## copy() diff --git a/docs/php/builtins/filesystem/dirname.md b/docs/php/builtins/filesystem/dirname.md index 85529de6a9..621a091d7f 100644 --- a/docs/php/builtins/filesystem/dirname.md +++ b/docs/php/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname()" description: "Returns a parent directory's path." sidebar: - order: 111 + order: 118 --- ## dirname() diff --git a/docs/php/builtins/filesystem/disk_free_space.md b/docs/php/builtins/filesystem/disk_free_space.md index 76f74f74b0..a7b1c79be2 100644 --- a/docs/php/builtins/filesystem/disk_free_space.md +++ b/docs/php/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space()" description: "Returns available space on filesystem or disk partition." sidebar: - order: 112 + order: 119 --- ## disk_free_space() diff --git a/docs/php/builtins/filesystem/disk_total_space.md b/docs/php/builtins/filesystem/disk_total_space.md index 0016ab846c..1ea865a96d 100644 --- a/docs/php/builtins/filesystem/disk_total_space.md +++ b/docs/php/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space()" description: "Returns the total size of a filesystem or disk partition." sidebar: - order: 113 + order: 120 --- ## disk_total_space() diff --git a/docs/php/builtins/filesystem/file_exists.md b/docs/php/builtins/filesystem/file_exists.md index 0e1f50aea6..db04e1ae28 100644 --- a/docs/php/builtins/filesystem/file_exists.md +++ b/docs/php/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists()" description: "Checks whether a file or directory exists." sidebar: - order: 114 + order: 121 --- ## file_exists() diff --git a/docs/php/builtins/filesystem/fileatime.md b/docs/php/builtins/filesystem/fileatime.md index 9546b12465..d89049f500 100644 --- a/docs/php/builtins/filesystem/fileatime.md +++ b/docs/php/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime()" description: "Gets last access time of file." sidebar: - order: 115 + order: 122 --- ## fileatime() diff --git a/docs/php/builtins/filesystem/filectime.md b/docs/php/builtins/filesystem/filectime.md index 9e705e961b..18bfacc709 100644 --- a/docs/php/builtins/filesystem/filectime.md +++ b/docs/php/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime()" description: "Gets inode change time of file." sidebar: - order: 116 + order: 123 --- ## filectime() diff --git a/docs/php/builtins/filesystem/filegroup.md b/docs/php/builtins/filesystem/filegroup.md index 2ac620138c..a423e088c8 100644 --- a/docs/php/builtins/filesystem/filegroup.md +++ b/docs/php/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup()" description: "Gets file group." sidebar: - order: 117 + order: 124 --- ## filegroup() diff --git a/docs/php/builtins/filesystem/fileinode.md b/docs/php/builtins/filesystem/fileinode.md index b57f83787b..c2cb4fa1dd 100644 --- a/docs/php/builtins/filesystem/fileinode.md +++ b/docs/php/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode()" description: "Gets file inode." sidebar: - order: 118 + order: 125 --- ## fileinode() diff --git a/docs/php/builtins/filesystem/filemtime.md b/docs/php/builtins/filesystem/filemtime.md index d450fe72e5..060ca6010b 100644 --- a/docs/php/builtins/filesystem/filemtime.md +++ b/docs/php/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime()" description: "Gets file modification time." sidebar: - order: 119 + order: 126 --- ## filemtime() diff --git a/docs/php/builtins/filesystem/fileowner.md b/docs/php/builtins/filesystem/fileowner.md index 3e9333890c..e6999f3e9e 100644 --- a/docs/php/builtins/filesystem/fileowner.md +++ b/docs/php/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner()" description: "Gets file owner." sidebar: - order: 120 + order: 127 --- ## fileowner() diff --git a/docs/php/builtins/filesystem/fileperms.md b/docs/php/builtins/filesystem/fileperms.md index 5e4ccef755..98177f4db2 100644 --- a/docs/php/builtins/filesystem/fileperms.md +++ b/docs/php/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms()" description: "Gets file permissions." sidebar: - order: 121 + order: 128 --- ## fileperms() diff --git a/docs/php/builtins/filesystem/filesize.md b/docs/php/builtins/filesystem/filesize.md index 8e533bf071..e192c6955a 100644 --- a/docs/php/builtins/filesystem/filesize.md +++ b/docs/php/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize()" description: "Gets file size." sidebar: - order: 122 + order: 129 --- ## filesize() diff --git a/docs/php/builtins/filesystem/filetype.md b/docs/php/builtins/filesystem/filetype.md index 3812f11dc2..0deb204e97 100644 --- a/docs/php/builtins/filesystem/filetype.md +++ b/docs/php/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype()" description: "Gets file type." sidebar: - order: 123 + order: 130 --- ## filetype() diff --git a/docs/php/builtins/filesystem/fnmatch.md b/docs/php/builtins/filesystem/fnmatch.md index 5db0b86691..8ab874f8dd 100644 --- a/docs/php/builtins/filesystem/fnmatch.md +++ b/docs/php/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch()" description: "Matches a filename against a pattern." sidebar: - order: 124 + order: 131 --- ## fnmatch() diff --git a/docs/php/builtins/filesystem/getcwd.md b/docs/php/builtins/filesystem/getcwd.md index 2445666337..6fd49da508 100644 --- a/docs/php/builtins/filesystem/getcwd.md +++ b/docs/php/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd()" description: "Gets the current working directory." sidebar: - order: 125 + order: 132 --- ## getcwd() diff --git a/docs/php/builtins/filesystem/getenv.md b/docs/php/builtins/filesystem/getenv.md index b88b242bc2..20c540b80d 100644 --- a/docs/php/builtins/filesystem/getenv.md +++ b/docs/php/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv()" description: "Gets the value of an environment variable." sidebar: - order: 126 + order: 133 --- ## getenv() diff --git a/docs/php/builtins/filesystem/glob.md b/docs/php/builtins/filesystem/glob.md index ab9a64c57b..380c6678e5 100644 --- a/docs/php/builtins/filesystem/glob.md +++ b/docs/php/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob()" description: "Finds pathnames matching a pattern." sidebar: - order: 127 + order: 134 --- ## glob() diff --git a/docs/php/builtins/filesystem/is_dir.md b/docs/php/builtins/filesystem/is_dir.md index 856d1e1c15..1f0ce929b8 100644 --- a/docs/php/builtins/filesystem/is_dir.md +++ b/docs/php/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir()" description: "Tells whether the filename is a directory." sidebar: - order: 128 + order: 135 --- ## is_dir() diff --git a/docs/php/builtins/filesystem/is_executable.md b/docs/php/builtins/filesystem/is_executable.md index c2cc09427a..6622cbd9ed 100644 --- a/docs/php/builtins/filesystem/is_executable.md +++ b/docs/php/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable()" description: "Tells whether the filename is executable." sidebar: - order: 129 + order: 136 --- ## is_executable() diff --git a/docs/php/builtins/filesystem/is_file.md b/docs/php/builtins/filesystem/is_file.md index fb961ecf2e..e393dc65cd 100644 --- a/docs/php/builtins/filesystem/is_file.md +++ b/docs/php/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file()" description: "Tells whether the filename is a regular file." sidebar: - order: 130 + order: 137 --- ## is_file() diff --git a/docs/php/builtins/filesystem/is_link.md b/docs/php/builtins/filesystem/is_link.md index 672b36615f..7df998e5c6 100644 --- a/docs/php/builtins/filesystem/is_link.md +++ b/docs/php/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link()" description: "Tells whether the filename is a symbolic link." sidebar: - order: 131 + order: 138 --- ## is_link() diff --git a/docs/php/builtins/filesystem/is_readable.md b/docs/php/builtins/filesystem/is_readable.md index 6f4ae6bc8b..b55289a561 100644 --- a/docs/php/builtins/filesystem/is_readable.md +++ b/docs/php/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable()" description: "Tells whether the filename is readable." sidebar: - order: 132 + order: 139 --- ## is_readable() diff --git a/docs/php/builtins/filesystem/is_writable.md b/docs/php/builtins/filesystem/is_writable.md index 718a8097e0..b14c00095d 100644 --- a/docs/php/builtins/filesystem/is_writable.md +++ b/docs/php/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable()" description: "Tells whether the filename is writable." sidebar: - order: 133 + order: 140 --- ## is_writable() diff --git a/docs/php/builtins/filesystem/is_writeable.md b/docs/php/builtins/filesystem/is_writeable.md index 3e15399531..4609609ebd 100644 --- a/docs/php/builtins/filesystem/is_writeable.md +++ b/docs/php/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable()" description: "Tells whether the filename is writable (alias of is_writable)." sidebar: - order: 134 + order: 141 --- ## is_writeable() diff --git a/docs/php/builtins/filesystem/lchgrp.md b/docs/php/builtins/filesystem/lchgrp.md index da391127bb..cd25ced053 100644 --- a/docs/php/builtins/filesystem/lchgrp.md +++ b/docs/php/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp()" description: "Changes group ownership of a symlink." sidebar: - order: 135 + order: 142 --- ## lchgrp() diff --git a/docs/php/builtins/filesystem/lchown.md b/docs/php/builtins/filesystem/lchown.md index bbd89e1877..55db7052ba 100644 --- a/docs/php/builtins/filesystem/lchown.md +++ b/docs/php/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown()" description: "Changes user ownership of a symlink." sidebar: - order: 136 + order: 143 --- ## lchown() diff --git a/docs/php/builtins/filesystem/link.md b/docs/php/builtins/filesystem/link.md index 80f3a6650a..14a4a0c329 100644 --- a/docs/php/builtins/filesystem/link.md +++ b/docs/php/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link()" description: "Creates a hard link." sidebar: - order: 137 + order: 144 --- ## link() diff --git a/docs/php/builtins/filesystem/linkinfo.md b/docs/php/builtins/filesystem/linkinfo.md index bbe945ae6e..26e5f3a0cf 100644 --- a/docs/php/builtins/filesystem/linkinfo.md +++ b/docs/php/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo()" description: "Gets information about a link." sidebar: - order: 138 + order: 145 --- ## linkinfo() diff --git a/docs/php/builtins/filesystem/lstat.md b/docs/php/builtins/filesystem/lstat.md index c19783a258..adf3de6f59 100644 --- a/docs/php/builtins/filesystem/lstat.md +++ b/docs/php/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat()" description: "Gives information about a file or symbolic link." sidebar: - order: 139 + order: 146 --- ## lstat() diff --git a/docs/php/builtins/filesystem/mkdir.md b/docs/php/builtins/filesystem/mkdir.md index 77b5b32688..52c2095363 100644 --- a/docs/php/builtins/filesystem/mkdir.md +++ b/docs/php/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir()" description: "Makes a directory." sidebar: - order: 140 + order: 147 --- ## mkdir() diff --git a/docs/php/builtins/filesystem/pathinfo.md b/docs/php/builtins/filesystem/pathinfo.md index 14a38095a1..a2cd4a72e8 100644 --- a/docs/php/builtins/filesystem/pathinfo.md +++ b/docs/php/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo()" description: "Returns information about a file path." sidebar: - order: 141 + order: 148 --- ## pathinfo() diff --git a/docs/php/builtins/filesystem/putenv.md b/docs/php/builtins/filesystem/putenv.md index 5645fa65e9..724d9e9cde 100644 --- a/docs/php/builtins/filesystem/putenv.md +++ b/docs/php/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv()" description: "Sets an environment variable." sidebar: - order: 142 + order: 149 --- ## putenv() diff --git a/docs/php/builtins/filesystem/readfile.md b/docs/php/builtins/filesystem/readfile.md index fc543a84b1..dd4bed708b 100644 --- a/docs/php/builtins/filesystem/readfile.md +++ b/docs/php/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile()" description: "Outputs a file." sidebar: - order: 143 + order: 150 --- ## readfile() diff --git a/docs/php/builtins/filesystem/readlink.md b/docs/php/builtins/filesystem/readlink.md index 19a2e35535..b8bdd8551e 100644 --- a/docs/php/builtins/filesystem/readlink.md +++ b/docs/php/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink()" description: "Returns the target of a symbolic link." sidebar: - order: 144 + order: 151 --- ## readlink() diff --git a/docs/php/builtins/filesystem/realpath.md b/docs/php/builtins/filesystem/realpath.md index d05822ed9f..20cf1399fa 100644 --- a/docs/php/builtins/filesystem/realpath.md +++ b/docs/php/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath()" description: "Returns canonicalized absolute pathname." sidebar: - order: 145 + order: 152 --- ## realpath() diff --git a/docs/php/builtins/filesystem/realpath_cache_get.md b/docs/php/builtins/filesystem/realpath_cache_get.md index 8463ccc41d..6fb88324e8 100644 --- a/docs/php/builtins/filesystem/realpath_cache_get.md +++ b/docs/php/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get()" description: "Returns realpath cache entries." sidebar: - order: 146 + order: 153 --- ## realpath_cache_get() diff --git a/docs/php/builtins/filesystem/realpath_cache_size.md b/docs/php/builtins/filesystem/realpath_cache_size.md index 7a7295dc81..066108dfa4 100644 --- a/docs/php/builtins/filesystem/realpath_cache_size.md +++ b/docs/php/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size()" description: "Returns the amount of memory used by the realpath cache." sidebar: - order: 147 + order: 154 --- ## realpath_cache_size() diff --git a/docs/php/builtins/filesystem/rename.md b/docs/php/builtins/filesystem/rename.md index d64c9d20d7..1659602fb6 100644 --- a/docs/php/builtins/filesystem/rename.md +++ b/docs/php/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename()" description: "Renames a file or directory." sidebar: - order: 148 + order: 155 --- ## rename() diff --git a/docs/php/builtins/filesystem/rmdir.md b/docs/php/builtins/filesystem/rmdir.md index 4c978eff12..6b82925379 100644 --- a/docs/php/builtins/filesystem/rmdir.md +++ b/docs/php/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir()" description: "Removes a directory." sidebar: - order: 149 + order: 156 --- ## rmdir() diff --git a/docs/php/builtins/filesystem/scandir.md b/docs/php/builtins/filesystem/scandir.md index b1d0700385..261bb32247 100644 --- a/docs/php/builtins/filesystem/scandir.md +++ b/docs/php/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir()" description: "Lists files and directories inside the specified path." sidebar: - order: 150 + order: 157 --- ## scandir() diff --git a/docs/php/builtins/filesystem/stat.md b/docs/php/builtins/filesystem/stat.md index 878546fcf2..cf05d92be8 100644 --- a/docs/php/builtins/filesystem/stat.md +++ b/docs/php/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat()" description: "Gives information about a file." sidebar: - order: 151 + order: 158 --- ## stat() diff --git a/docs/php/builtins/filesystem/symlink.md b/docs/php/builtins/filesystem/symlink.md index 68eba1078c..5cfef26c73 100644 --- a/docs/php/builtins/filesystem/symlink.md +++ b/docs/php/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink()" description: "Creates a symbolic link." sidebar: - order: 152 + order: 159 --- ## symlink() diff --git a/docs/php/builtins/filesystem/sys_get_temp_dir.md b/docs/php/builtins/filesystem/sys_get_temp_dir.md index 2876375c5a..492d5ff015 100644 --- a/docs/php/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/php/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir()" description: "Returns the directory path used for temporary files." sidebar: - order: 153 + order: 160 --- ## sys_get_temp_dir() diff --git a/docs/php/builtins/filesystem/tempnam.md b/docs/php/builtins/filesystem/tempnam.md index 3a6f82fa74..7b707f085c 100644 --- a/docs/php/builtins/filesystem/tempnam.md +++ b/docs/php/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam()" description: "Creates a file with a unique filename." sidebar: - order: 154 + order: 161 --- ## tempnam() diff --git a/docs/php/builtins/filesystem/tmpfile.md b/docs/php/builtins/filesystem/tmpfile.md index d1fd330bdc..2bdd1b496f 100644 --- a/docs/php/builtins/filesystem/tmpfile.md +++ b/docs/php/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile()" description: "Creates a temporary file." sidebar: - order: 155 + order: 162 --- ## tmpfile() diff --git a/docs/php/builtins/filesystem/touch.md b/docs/php/builtins/filesystem/touch.md index cfd4972789..80b6a64127 100644 --- a/docs/php/builtins/filesystem/touch.md +++ b/docs/php/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch()" description: "Sets access and modification time of a file." sidebar: - order: 156 + order: 163 --- ## touch() diff --git a/docs/php/builtins/filesystem/umask.md b/docs/php/builtins/filesystem/umask.md index 2dd9a66ab9..87912775e8 100644 --- a/docs/php/builtins/filesystem/umask.md +++ b/docs/php/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask()" description: "Changes the current umask." sidebar: - order: 157 + order: 164 --- ## umask() diff --git a/docs/php/builtins/filesystem/unlink.md b/docs/php/builtins/filesystem/unlink.md index a46db3cfc1..4e32d25ca6 100644 --- a/docs/php/builtins/filesystem/unlink.md +++ b/docs/php/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink()" description: "Deletes a file." sidebar: - order: 158 + order: 165 --- ## unlink() diff --git a/docs/php/builtins/io.md b/docs/php/builtins/io.md index fa81553e34..ba53416905 100644 --- a/docs/php/builtins/io.md +++ b/docs/php/builtins/io.md @@ -17,8 +17,8 @@ sidebar: | [`fgetc()`](./io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | | [`fgetcsv()`](./io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | | [`fgets()`](./io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | -| [`file()`](./io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | -| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./io/file.md) | `(string $filename, int $flags = 0): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename, bool $use_include_path = false, mixed $context = null, int $offset = 0, int $length = null): mixed` | `mixed` | ✓ | ✓ | | [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | | [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | | [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | diff --git a/docs/php/builtins/io/closedir.md b/docs/php/builtins/io/closedir.md index 919aae28c7..12256b8694 100644 --- a/docs/php/builtins/io/closedir.md +++ b/docs/php/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir()" description: "Closes directory handle." sidebar: - order: 159 + order: 166 --- ## closedir() diff --git a/docs/php/builtins/io/fclose.md b/docs/php/builtins/io/fclose.md index df90be6c4c..9c3bda5d3a 100644 --- a/docs/php/builtins/io/fclose.md +++ b/docs/php/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose()" description: "Closes an open file pointer." sidebar: - order: 160 + order: 167 --- ## fclose() diff --git a/docs/php/builtins/io/fdatasync.md b/docs/php/builtins/io/fdatasync.md index c1041629b5..cc95e3f87d 100644 --- a/docs/php/builtins/io/fdatasync.md +++ b/docs/php/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync()" description: "Synchronizes data (but not meta-data) to file." sidebar: - order: 161 + order: 168 --- ## fdatasync() diff --git a/docs/php/builtins/io/feof.md b/docs/php/builtins/io/feof.md index 14a06122ec..8a8c3b8f3e 100644 --- a/docs/php/builtins/io/feof.md +++ b/docs/php/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof()" description: "Tests for end-of-file on a file pointer." sidebar: - order: 162 + order: 169 --- ## feof() diff --git a/docs/php/builtins/io/fflush.md b/docs/php/builtins/io/fflush.md index 045a12e717..5bedfbe96c 100644 --- a/docs/php/builtins/io/fflush.md +++ b/docs/php/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush()" description: "Flushes the output to a file." sidebar: - order: 163 + order: 170 --- ## fflush() diff --git a/docs/php/builtins/io/fgetc.md b/docs/php/builtins/io/fgetc.md index 82d01a286a..f6a750735e 100644 --- a/docs/php/builtins/io/fgetc.md +++ b/docs/php/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc()" description: "Gets a character from the given file pointer." sidebar: - order: 164 + order: 171 --- ## fgetc() diff --git a/docs/php/builtins/io/fgetcsv.md b/docs/php/builtins/io/fgetcsv.md index bd76eb949e..94cc774665 100644 --- a/docs/php/builtins/io/fgetcsv.md +++ b/docs/php/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv()" description: "Gets line from file pointer and parse for CSV fields." sidebar: - order: 165 + order: 172 --- ## fgetcsv() diff --git a/docs/php/builtins/io/fgets.md b/docs/php/builtins/io/fgets.md index 758ebda637..a23087bc5c 100644 --- a/docs/php/builtins/io/fgets.md +++ b/docs/php/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets()" description: "Gets line from file pointer." sidebar: - order: 166 + order: 173 --- ## fgets() diff --git a/docs/php/builtins/io/file.md b/docs/php/builtins/io/file.md index 9de685344d..281f3d3015 100644 --- a/docs/php/builtins/io/file.md +++ b/docs/php/builtins/io/file.md @@ -2,19 +2,20 @@ title: "file()" description: "Reads an entire file into an array." sidebar: - order: 167 + order: 174 --- ## file() ```php -function file(string $filename): array +function file(string $filename, int $flags = 0): array ``` Reads an entire file into an array. **Parameters**: - `$filename` (`string`) +- `$flags` (`int`), default `0`, optional **Returns**: `array` diff --git a/docs/php/builtins/io/file_get_contents.md b/docs/php/builtins/io/file_get_contents.md index 9ea3382235..52e6f42144 100644 --- a/docs/php/builtins/io/file_get_contents.md +++ b/docs/php/builtins/io/file_get_contents.md @@ -2,19 +2,23 @@ title: "file_get_contents()" description: "Reads an entire file into a string." sidebar: - order: 168 + order: 175 --- ## file_get_contents() ```php -function file_get_contents(string $filename): mixed +function file_get_contents(string $filename, bool $use_include_path = false, mixed $context = null, int $offset = 0, int $length = null): mixed ``` Reads an entire file into a string. **Parameters**: - `$filename` (`string`) +- `$use_include_path` (`bool`), default `false`, optional +- `$context` (`mixed`), default `null`, optional +- `$offset` (`int`), default `0`, optional +- `$length` (`int`), default `null`, optional **Returns**: `mixed` diff --git a/docs/php/builtins/io/file_put_contents.md b/docs/php/builtins/io/file_put_contents.md index 73d793c0c1..6e58450c85 100644 --- a/docs/php/builtins/io/file_put_contents.md +++ b/docs/php/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents()" description: "Writes data to a file." sidebar: - order: 169 + order: 176 --- ## file_put_contents() diff --git a/docs/php/builtins/io/flock.md b/docs/php/builtins/io/flock.md index c7b78263d1..640c301747 100644 --- a/docs/php/builtins/io/flock.md +++ b/docs/php/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock()" description: "Portable advisory file locking." sidebar: - order: 170 + order: 177 --- ## flock() diff --git a/docs/php/builtins/io/fopen.md b/docs/php/builtins/io/fopen.md index 7e1a91f4fe..664282e801 100644 --- a/docs/php/builtins/io/fopen.md +++ b/docs/php/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen()" description: "Opens file or URL." sidebar: - order: 171 + order: 178 --- ## fopen() diff --git a/docs/php/builtins/io/fpassthru.md b/docs/php/builtins/io/fpassthru.md index 2e22b0428c..eedc4f1b19 100644 --- a/docs/php/builtins/io/fpassthru.md +++ b/docs/php/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru()" description: "Output all remaining data on a file pointer." sidebar: - order: 172 + order: 179 --- ## fpassthru() diff --git a/docs/php/builtins/io/fprintf.md b/docs/php/builtins/io/fprintf.md index 4eb6998668..9b6d4aa42c 100644 --- a/docs/php/builtins/io/fprintf.md +++ b/docs/php/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf()" description: "Write a formatted string to a stream." sidebar: - order: 173 + order: 180 --- ## fprintf() diff --git a/docs/php/builtins/io/fputcsv.md b/docs/php/builtins/io/fputcsv.md index a1b5b924b8..3675b6839e 100644 --- a/docs/php/builtins/io/fputcsv.md +++ b/docs/php/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv()" description: "Format line as CSV and write to file pointer." sidebar: - order: 174 + order: 181 --- ## fputcsv() diff --git a/docs/php/builtins/io/fread.md b/docs/php/builtins/io/fread.md index 6b43dd9fa8..7d35558047 100644 --- a/docs/php/builtins/io/fread.md +++ b/docs/php/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread()" description: "Binary-safe file read." sidebar: - order: 175 + order: 182 --- ## fread() diff --git a/docs/php/builtins/io/fscanf.md b/docs/php/builtins/io/fscanf.md index 7fdb54cdeb..8c61755aec 100644 --- a/docs/php/builtins/io/fscanf.md +++ b/docs/php/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf()" description: "Parses input from a file according to a format." sidebar: - order: 176 + order: 183 --- ## fscanf() diff --git a/docs/php/builtins/io/fseek.md b/docs/php/builtins/io/fseek.md index 779b0b5887..38e9b2821a 100644 --- a/docs/php/builtins/io/fseek.md +++ b/docs/php/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek()" description: "Seeks on a file pointer." sidebar: - order: 177 + order: 184 --- ## fseek() diff --git a/docs/php/builtins/io/fstat.md b/docs/php/builtins/io/fstat.md index c11469bf3f..65f73c520a 100644 --- a/docs/php/builtins/io/fstat.md +++ b/docs/php/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat()" description: "Gets information about a file using an open file pointer." sidebar: - order: 178 + order: 185 --- ## fstat() diff --git a/docs/php/builtins/io/fsync.md b/docs/php/builtins/io/fsync.md index c59e4ada03..9b469fc863 100644 --- a/docs/php/builtins/io/fsync.md +++ b/docs/php/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync()" description: "Synchronizes changes to the file (including meta-data)." sidebar: - order: 179 + order: 186 --- ## fsync() diff --git a/docs/php/builtins/io/ftell.md b/docs/php/builtins/io/ftell.md index 1df988c8e1..ee2d17f0bf 100644 --- a/docs/php/builtins/io/ftell.md +++ b/docs/php/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell()" description: "Returns the current position of the file read/write pointer." sidebar: - order: 180 + order: 187 --- ## ftell() diff --git a/docs/php/builtins/io/ftruncate.md b/docs/php/builtins/io/ftruncate.md index 9eb0a6ca9a..cc39e5a1c6 100644 --- a/docs/php/builtins/io/ftruncate.md +++ b/docs/php/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate()" description: "Truncates a file to a given length." sidebar: - order: 181 + order: 188 --- ## ftruncate() diff --git a/docs/php/builtins/io/fwrite.md b/docs/php/builtins/io/fwrite.md index c0d7075e2a..5d2447581f 100644 --- a/docs/php/builtins/io/fwrite.md +++ b/docs/php/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite()" description: "Binary-safe file write." sidebar: - order: 182 + order: 189 --- ## fwrite() diff --git a/docs/php/builtins/io/gethostbyaddr.md b/docs/php/builtins/io/gethostbyaddr.md index a645a0e197..7d528c9b30 100644 --- a/docs/php/builtins/io/gethostbyaddr.md +++ b/docs/php/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr()" description: "Gets the Internet host name corresponding to a given IP address." sidebar: - order: 183 + order: 190 --- ## gethostbyaddr() diff --git a/docs/php/builtins/io/gethostbyname.md b/docs/php/builtins/io/gethostbyname.md index 19515d2304..0d36def692 100644 --- a/docs/php/builtins/io/gethostbyname.md +++ b/docs/php/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname()" description: "Gets the IPv4 address corresponding to the given Internet host name." sidebar: - order: 184 + order: 191 --- ## gethostbyname() diff --git a/docs/php/builtins/io/gethostname.md b/docs/php/builtins/io/gethostname.md index 69765d957d..2aa25c0088 100644 --- a/docs/php/builtins/io/gethostname.md +++ b/docs/php/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname()" description: "Gets the standard host name for the local machine." sidebar: - order: 185 + order: 192 --- ## gethostname() diff --git a/docs/php/builtins/io/getprotobyname.md b/docs/php/builtins/io/getprotobyname.md index 99511bab52..050f20e97e 100644 --- a/docs/php/builtins/io/getprotobyname.md +++ b/docs/php/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname()" description: "Gets the protocol number associated with the given protocol name." sidebar: - order: 186 + order: 193 --- ## getprotobyname() diff --git a/docs/php/builtins/io/getprotobynumber.md b/docs/php/builtins/io/getprotobynumber.md index 72917f0b80..73974adac6 100644 --- a/docs/php/builtins/io/getprotobynumber.md +++ b/docs/php/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber()" description: "Gets the protocol name associated with the given protocol number." sidebar: - order: 187 + order: 194 --- ## getprotobynumber() diff --git a/docs/php/builtins/io/getservbyname.md b/docs/php/builtins/io/getservbyname.md index b33c475535..17f6a01bbd 100644 --- a/docs/php/builtins/io/getservbyname.md +++ b/docs/php/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname()" description: "Gets port number associated with an Internet service and protocol." sidebar: - order: 188 + order: 195 --- ## getservbyname() diff --git a/docs/php/builtins/io/getservbyport.md b/docs/php/builtins/io/getservbyport.md index 12ec92b980..4e97df5d34 100644 --- a/docs/php/builtins/io/getservbyport.md +++ b/docs/php/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport()" description: "Gets the Internet service that corresponds to a port and protocol." sidebar: - order: 189 + order: 196 --- ## getservbyport() diff --git a/docs/php/builtins/io/hash_file.md b/docs/php/builtins/io/hash_file.md index 0654127d6b..7e319840e7 100644 --- a/docs/php/builtins/io/hash_file.md +++ b/docs/php/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file()" description: "Generates a hash value using the contents of a given file." sidebar: - order: 190 + order: 197 --- ## hash_file() diff --git a/docs/php/builtins/io/ob_clean.md b/docs/php/builtins/io/ob_clean.md index 4eea28a2ec..135e4ed4f8 100644 --- a/docs/php/builtins/io/ob_clean.md +++ b/docs/php/builtins/io/ob_clean.md @@ -2,7 +2,7 @@ title: "ob_clean()" description: "Cleans (erases) the contents of the active output buffer." sidebar: - order: 191 + order: 198 --- ## ob_clean() diff --git a/docs/php/builtins/io/ob_end_clean.md b/docs/php/builtins/io/ob_end_clean.md index 1b2053b445..49b925ca99 100644 --- a/docs/php/builtins/io/ob_end_clean.md +++ b/docs/php/builtins/io/ob_end_clean.md @@ -2,7 +2,7 @@ title: "ob_end_clean()" description: "Cleans (erases) the contents of the active output buffer and turns it off." sidebar: - order: 192 + order: 199 --- ## ob_end_clean() diff --git a/docs/php/builtins/io/ob_end_flush.md b/docs/php/builtins/io/ob_end_flush.md index 333528b946..dca3e73625 100644 --- a/docs/php/builtins/io/ob_end_flush.md +++ b/docs/php/builtins/io/ob_end_flush.md @@ -2,7 +2,7 @@ title: "ob_end_flush()" description: "Flushes (sends) the contents of the active output buffer and turns it off." sidebar: - order: 193 + order: 200 --- ## ob_end_flush() diff --git a/docs/php/builtins/io/ob_flush.md b/docs/php/builtins/io/ob_flush.md index 9f031affc0..879bd4d962 100644 --- a/docs/php/builtins/io/ob_flush.md +++ b/docs/php/builtins/io/ob_flush.md @@ -2,7 +2,7 @@ title: "ob_flush()" description: "Flushes (sends) the contents of the active output buffer." sidebar: - order: 194 + order: 201 --- ## ob_flush() diff --git a/docs/php/builtins/io/ob_get_clean.md b/docs/php/builtins/io/ob_get_clean.md index de80ff90fe..c04f0ea488 100644 --- a/docs/php/builtins/io/ob_get_clean.md +++ b/docs/php/builtins/io/ob_get_clean.md @@ -2,7 +2,7 @@ title: "ob_get_clean()" description: "Gets the current buffer contents and deletes the current output buffer." sidebar: - order: 195 + order: 202 --- ## ob_get_clean() diff --git a/docs/php/builtins/io/ob_get_contents.md b/docs/php/builtins/io/ob_get_contents.md index 0cbe65f9c1..f19b7d1ee8 100644 --- a/docs/php/builtins/io/ob_get_contents.md +++ b/docs/php/builtins/io/ob_get_contents.md @@ -2,7 +2,7 @@ title: "ob_get_contents()" description: "Returns the contents of the output buffer." sidebar: - order: 196 + order: 203 --- ## ob_get_contents() diff --git a/docs/php/builtins/io/ob_get_flush.md b/docs/php/builtins/io/ob_get_flush.md index 4f3b7adfa1..26fc3dc4bd 100644 --- a/docs/php/builtins/io/ob_get_flush.md +++ b/docs/php/builtins/io/ob_get_flush.md @@ -2,7 +2,7 @@ title: "ob_get_flush()" description: "Flushes the output buffer, returns it as a string and turns off output buffering." sidebar: - order: 197 + order: 204 --- ## ob_get_flush() diff --git a/docs/php/builtins/io/ob_get_length.md b/docs/php/builtins/io/ob_get_length.md index 17e9db978d..4f0c6acee9 100644 --- a/docs/php/builtins/io/ob_get_length.md +++ b/docs/php/builtins/io/ob_get_length.md @@ -2,7 +2,7 @@ title: "ob_get_length()" description: "Returns the length of the output buffer." sidebar: - order: 198 + order: 205 --- ## ob_get_length() diff --git a/docs/php/builtins/io/ob_get_level.md b/docs/php/builtins/io/ob_get_level.md index ef21c9d316..6499e1d1eb 100644 --- a/docs/php/builtins/io/ob_get_level.md +++ b/docs/php/builtins/io/ob_get_level.md @@ -2,7 +2,7 @@ title: "ob_get_level()" description: "Returns the nesting level of the output buffering mechanism." sidebar: - order: 199 + order: 206 --- ## ob_get_level() diff --git a/docs/php/builtins/io/ob_get_status.md b/docs/php/builtins/io/ob_get_status.md index 435903b6a6..c4e42fb746 100644 --- a/docs/php/builtins/io/ob_get_status.md +++ b/docs/php/builtins/io/ob_get_status.md @@ -2,7 +2,7 @@ title: "ob_get_status()" description: "Gets status of output buffers." sidebar: - order: 200 + order: 207 --- ## ob_get_status() diff --git a/docs/php/builtins/io/ob_implicit_flush.md b/docs/php/builtins/io/ob_implicit_flush.md index ecdf405696..748579ed12 100644 --- a/docs/php/builtins/io/ob_implicit_flush.md +++ b/docs/php/builtins/io/ob_implicit_flush.md @@ -2,7 +2,7 @@ title: "ob_implicit_flush()" description: "Turns implicit flush on/off." sidebar: - order: 201 + order: 208 --- ## ob_implicit_flush() diff --git a/docs/php/builtins/io/ob_list_handlers.md b/docs/php/builtins/io/ob_list_handlers.md index 1cbf95f704..05906e695b 100644 --- a/docs/php/builtins/io/ob_list_handlers.md +++ b/docs/php/builtins/io/ob_list_handlers.md @@ -2,7 +2,7 @@ title: "ob_list_handlers()" description: "Lists all output handlers in use." sidebar: - order: 202 + order: 209 --- ## ob_list_handlers() diff --git a/docs/php/builtins/io/ob_start.md b/docs/php/builtins/io/ob_start.md index 192a277600..bceea753ef 100644 --- a/docs/php/builtins/io/ob_start.md +++ b/docs/php/builtins/io/ob_start.md @@ -2,7 +2,7 @@ title: "ob_start()" description: "Turns on output buffering." sidebar: - order: 203 + order: 210 --- ## ob_start() diff --git a/docs/php/builtins/io/opendir.md b/docs/php/builtins/io/opendir.md index d0851e9907..f4de271e9c 100644 --- a/docs/php/builtins/io/opendir.md +++ b/docs/php/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir()" description: "Open directory handle." sidebar: - order: 204 + order: 211 --- ## opendir() diff --git a/docs/php/builtins/io/readdir.md b/docs/php/builtins/io/readdir.md index f9870ed5f0..6f31313225 100644 --- a/docs/php/builtins/io/readdir.md +++ b/docs/php/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir()" description: "Read entry from directory handle." sidebar: - order: 205 + order: 212 --- ## readdir() diff --git a/docs/php/builtins/io/rewind.md b/docs/php/builtins/io/rewind.md index 1293bf48f4..483b62ed0d 100644 --- a/docs/php/builtins/io/rewind.md +++ b/docs/php/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind()" description: "Rewind the position of a file pointer." sidebar: - order: 206 + order: 213 --- ## rewind() diff --git a/docs/php/builtins/io/rewinddir.md b/docs/php/builtins/io/rewinddir.md index a4b5a7a778..c64bb31da7 100644 --- a/docs/php/builtins/io/rewinddir.md +++ b/docs/php/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir()" description: "Rewind directory handle." sidebar: - order: 207 + order: 214 --- ## rewinddir() diff --git a/docs/php/builtins/io/stream_bucket_make_writeable.md b/docs/php/builtins/io/stream_bucket_make_writeable.md index 71fa332113..c67e02140a 100644 --- a/docs/php/builtins/io/stream_bucket_make_writeable.md +++ b/docs/php/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable()" description: "Returns a bucket object from the brigade for use in a stream filter." sidebar: - order: 208 + order: 215 --- ## stream_bucket_make_writeable() diff --git a/docs/php/builtins/io/stream_bucket_new.md b/docs/php/builtins/io/stream_bucket_new.md index 3c827c30ea..58df712588 100644 --- a/docs/php/builtins/io/stream_bucket_new.md +++ b/docs/php/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new()" description: "Creates a new bucket for use in a stream filter." sidebar: - order: 209 + order: 216 --- ## stream_bucket_new() diff --git a/docs/php/builtins/io/stream_context_create.md b/docs/php/builtins/io/stream_context_create.md index 1753f99d5e..f3b87b536b 100644 --- a/docs/php/builtins/io/stream_context_create.md +++ b/docs/php/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create()" description: "Creates a stream context." sidebar: - order: 210 + order: 217 --- ## stream_context_create() diff --git a/docs/php/builtins/io/stream_context_get_default.md b/docs/php/builtins/io/stream_context_get_default.md index ba0e414e6a..81c5af249b 100644 --- a/docs/php/builtins/io/stream_context_get_default.md +++ b/docs/php/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default()" description: "Retrieves the default stream context." sidebar: - order: 211 + order: 218 --- ## stream_context_get_default() diff --git a/docs/php/builtins/io/stream_context_get_options.md b/docs/php/builtins/io/stream_context_get_options.md index fd0a44e8ed..6bf13d93ae 100644 --- a/docs/php/builtins/io/stream_context_get_options.md +++ b/docs/php/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options()" description: "Retrieves options for the specified stream context." sidebar: - order: 212 + order: 219 --- ## stream_context_get_options() diff --git a/docs/php/builtins/io/stream_context_get_params.md b/docs/php/builtins/io/stream_context_get_params.md index cb0d1310b0..1f21b8c7f5 100644 --- a/docs/php/builtins/io/stream_context_get_params.md +++ b/docs/php/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params()" description: "Retrieves parameters from the specified stream context." sidebar: - order: 213 + order: 220 --- ## stream_context_get_params() diff --git a/docs/php/builtins/io/stream_context_set_default.md b/docs/php/builtins/io/stream_context_set_default.md index 80dcc93b67..6a30eda191 100644 --- a/docs/php/builtins/io/stream_context_set_default.md +++ b/docs/php/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default()" description: "Sets the default stream context." sidebar: - order: 214 + order: 221 --- ## stream_context_set_default() diff --git a/docs/php/builtins/io/stream_context_set_option.md b/docs/php/builtins/io/stream_context_set_option.md index 4a808fcdfa..5d29f23383 100644 --- a/docs/php/builtins/io/stream_context_set_option.md +++ b/docs/php/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option()" description: "Sets an option on the specified context." sidebar: - order: 215 + order: 222 --- ## stream_context_set_option() diff --git a/docs/php/builtins/io/stream_context_set_params.md b/docs/php/builtins/io/stream_context_set_params.md index 8606f4a59b..9dc022ee9a 100644 --- a/docs/php/builtins/io/stream_context_set_params.md +++ b/docs/php/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params()" description: "Sets parameters on the specified context." sidebar: - order: 216 + order: 223 --- ## stream_context_set_params() diff --git a/docs/php/builtins/io/stream_copy_to_stream.md b/docs/php/builtins/io/stream_copy_to_stream.md index b7af43503e..0909fbf533 100644 --- a/docs/php/builtins/io/stream_copy_to_stream.md +++ b/docs/php/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream()" description: "Copies data from one stream to another." sidebar: - order: 217 + order: 224 --- ## stream_copy_to_stream() diff --git a/docs/php/builtins/io/stream_filter_register.md b/docs/php/builtins/io/stream_filter_register.md index b2c7187f24..042d443b5f 100644 --- a/docs/php/builtins/io/stream_filter_register.md +++ b/docs/php/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register()" description: "Registers a user-defined stream filter." sidebar: - order: 218 + order: 225 --- ## stream_filter_register() diff --git a/docs/php/builtins/io/stream_filter_remove.md b/docs/php/builtins/io/stream_filter_remove.md index 937cf01a15..8fcdef7d99 100644 --- a/docs/php/builtins/io/stream_filter_remove.md +++ b/docs/php/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove()" description: "Removes a filter from a stream." sidebar: - order: 219 + order: 226 --- ## stream_filter_remove() diff --git a/docs/php/builtins/io/stream_get_contents.md b/docs/php/builtins/io/stream_get_contents.md index b382ae88e6..8f8c1c9db9 100644 --- a/docs/php/builtins/io/stream_get_contents.md +++ b/docs/php/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents()" description: "Reads remainder of a stream into a string." sidebar: - order: 220 + order: 227 --- ## stream_get_contents() diff --git a/docs/php/builtins/io/stream_get_filters.md b/docs/php/builtins/io/stream_get_filters.md index cdbffc7d8b..16fce9bab9 100644 --- a/docs/php/builtins/io/stream_get_filters.md +++ b/docs/php/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters()" description: "Retrieves list of registered filters." sidebar: - order: 221 + order: 228 --- ## stream_get_filters() diff --git a/docs/php/builtins/io/stream_get_line.md b/docs/php/builtins/io/stream_get_line.md index 8f87cf60ad..934458d1cf 100644 --- a/docs/php/builtins/io/stream_get_line.md +++ b/docs/php/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line()" description: "Gets line from stream resource up to a given delimiter." sidebar: - order: 222 + order: 229 --- ## stream_get_line() diff --git a/docs/php/builtins/io/stream_get_meta_data.md b/docs/php/builtins/io/stream_get_meta_data.md index 2752fd9cc4..d005f09f51 100644 --- a/docs/php/builtins/io/stream_get_meta_data.md +++ b/docs/php/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data()" description: "Retrieves metadata from streams/file pointers." sidebar: - order: 223 + order: 230 --- ## stream_get_meta_data() diff --git a/docs/php/builtins/io/stream_get_transports.md b/docs/php/builtins/io/stream_get_transports.md index 38d6af2640..72ff3d6753 100644 --- a/docs/php/builtins/io/stream_get_transports.md +++ b/docs/php/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports()" description: "Retrieves list of registered socket transports." sidebar: - order: 224 + order: 231 --- ## stream_get_transports() diff --git a/docs/php/builtins/io/stream_get_wrappers.md b/docs/php/builtins/io/stream_get_wrappers.md index 2f42c79b2f..77850696e0 100644 --- a/docs/php/builtins/io/stream_get_wrappers.md +++ b/docs/php/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers()" description: "Retrieves list of registered streams." sidebar: - order: 225 + order: 232 --- ## stream_get_wrappers() diff --git a/docs/php/builtins/io/stream_is_local.md b/docs/php/builtins/io/stream_is_local.md index dd6f1d336f..e76248810d 100644 --- a/docs/php/builtins/io/stream_is_local.md +++ b/docs/php/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local()" description: "Checks if a stream is a local stream." sidebar: - order: 226 + order: 233 --- ## stream_is_local() diff --git a/docs/php/builtins/io/stream_isatty.md b/docs/php/builtins/io/stream_isatty.md index 5619535d95..b43da8347c 100644 --- a/docs/php/builtins/io/stream_isatty.md +++ b/docs/php/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty()" description: "Checks if a stream is a TTY." sidebar: - order: 227 + order: 234 --- ## stream_isatty() diff --git a/docs/php/builtins/io/stream_resolve_include_path.md b/docs/php/builtins/io/stream_resolve_include_path.md index a45e4535a8..c1285d817f 100644 --- a/docs/php/builtins/io/stream_resolve_include_path.md +++ b/docs/php/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path()" description: "Resolves filename against the include path." sidebar: - order: 228 + order: 235 --- ## stream_resolve_include_path() diff --git a/docs/php/builtins/io/stream_select.md b/docs/php/builtins/io/stream_select.md index 4870e48170..3ec5cf3311 100644 --- a/docs/php/builtins/io/stream_select.md +++ b/docs/php/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select()" description: "Runs the equivalent of the select() system call on the given arrays of streams." sidebar: - order: 229 + order: 236 --- ## stream_select() diff --git a/docs/php/builtins/io/stream_set_blocking.md b/docs/php/builtins/io/stream_set_blocking.md index cd0fcab646..68b7ec7f6d 100644 --- a/docs/php/builtins/io/stream_set_blocking.md +++ b/docs/php/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking()" description: "Sets blocking/non-blocking mode on a stream." sidebar: - order: 230 + order: 237 --- ## stream_set_blocking() diff --git a/docs/php/builtins/io/stream_set_chunk_size.md b/docs/php/builtins/io/stream_set_chunk_size.md index 2b17b29cea..24602270ab 100644 --- a/docs/php/builtins/io/stream_set_chunk_size.md +++ b/docs/php/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size()" description: "Sets the read chunk size on a stream." sidebar: - order: 231 + order: 238 --- ## stream_set_chunk_size() diff --git a/docs/php/builtins/io/stream_set_read_buffer.md b/docs/php/builtins/io/stream_set_read_buffer.md index 412be4b523..8adaf20e7b 100644 --- a/docs/php/builtins/io/stream_set_read_buffer.md +++ b/docs/php/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer()" description: "Sets the read file buffering on a stream." sidebar: - order: 232 + order: 239 --- ## stream_set_read_buffer() diff --git a/docs/php/builtins/io/stream_set_timeout.md b/docs/php/builtins/io/stream_set_timeout.md index fa676821c5..aa4797ed4b 100644 --- a/docs/php/builtins/io/stream_set_timeout.md +++ b/docs/php/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout()" description: "Sets timeout period on a stream." sidebar: - order: 233 + order: 240 --- ## stream_set_timeout() diff --git a/docs/php/builtins/io/stream_set_write_buffer.md b/docs/php/builtins/io/stream_set_write_buffer.md index 0c65b923d3..a59280c998 100644 --- a/docs/php/builtins/io/stream_set_write_buffer.md +++ b/docs/php/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer()" description: "Sets the write file buffering on a stream." sidebar: - order: 234 + order: 241 --- ## stream_set_write_buffer() diff --git a/docs/php/builtins/io/stream_socket_accept.md b/docs/php/builtins/io/stream_socket_accept.md index c0ea571d00..0db650911c 100644 --- a/docs/php/builtins/io/stream_socket_accept.md +++ b/docs/php/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept()" description: "Accept a connection on a socket created by stream_socket_server()." sidebar: - order: 235 + order: 242 --- ## stream_socket_accept() diff --git a/docs/php/builtins/io/stream_socket_client.md b/docs/php/builtins/io/stream_socket_client.md index 5a93ae1c92..7c9cc8c3c3 100644 --- a/docs/php/builtins/io/stream_socket_client.md +++ b/docs/php/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 236 + order: 243 --- ## stream_socket_client() diff --git a/docs/php/builtins/io/stream_socket_enable_crypto.md b/docs/php/builtins/io/stream_socket_enable_crypto.md index 2011629b29..f85143509e 100644 --- a/docs/php/builtins/io/stream_socket_enable_crypto.md +++ b/docs/php/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto()" description: "Turns encryption on/off on an already connected socket." sidebar: - order: 237 + order: 244 --- ## stream_socket_enable_crypto() diff --git a/docs/php/builtins/io/stream_socket_get_name.md b/docs/php/builtins/io/stream_socket_get_name.md index 36d6c3027d..99328d9a73 100644 --- a/docs/php/builtins/io/stream_socket_get_name.md +++ b/docs/php/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name()" description: "Retrieve the name of the local or remote sockets." sidebar: - order: 238 + order: 245 --- ## stream_socket_get_name() diff --git a/docs/php/builtins/io/stream_socket_pair.md b/docs/php/builtins/io/stream_socket_pair.md index f97883661c..b8e62c8cab 100644 --- a/docs/php/builtins/io/stream_socket_pair.md +++ b/docs/php/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair()" description: "Creates a pair of connected, indistinguishable socket streams." sidebar: - order: 239 + order: 246 --- ## stream_socket_pair() diff --git a/docs/php/builtins/io/stream_socket_recvfrom.md b/docs/php/builtins/io/stream_socket_recvfrom.md index 0ea8687d35..3c3ae7d149 100644 --- a/docs/php/builtins/io/stream_socket_recvfrom.md +++ b/docs/php/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom()" description: "Receives data from a socket, connected or not." sidebar: - order: 240 + order: 247 --- ## stream_socket_recvfrom() diff --git a/docs/php/builtins/io/stream_socket_sendto.md b/docs/php/builtins/io/stream_socket_sendto.md index 47eec7817e..a8d4c3837b 100644 --- a/docs/php/builtins/io/stream_socket_sendto.md +++ b/docs/php/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto()" description: "Sends a message to a socket, whether it is connected or not." sidebar: - order: 241 + order: 248 --- ## stream_socket_sendto() diff --git a/docs/php/builtins/io/stream_socket_server.md b/docs/php/builtins/io/stream_socket_server.md index c50c679c6f..a7b771e742 100644 --- a/docs/php/builtins/io/stream_socket_server.md +++ b/docs/php/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server()" description: "Create an Internet or Unix domain server socket." sidebar: - order: 242 + order: 249 --- ## stream_socket_server() diff --git a/docs/php/builtins/io/stream_socket_shutdown.md b/docs/php/builtins/io/stream_socket_shutdown.md index f1e5d0bcc5..c0f00b671b 100644 --- a/docs/php/builtins/io/stream_socket_shutdown.md +++ b/docs/php/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown()" description: "Shutdown a full-duplex connection." sidebar: - order: 243 + order: 250 --- ## stream_socket_shutdown() diff --git a/docs/php/builtins/io/stream_supports_lock.md b/docs/php/builtins/io/stream_supports_lock.md index 47424d7f5e..2d0b8a71f1 100644 --- a/docs/php/builtins/io/stream_supports_lock.md +++ b/docs/php/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock()" description: "Tells whether the stream supports locking." sidebar: - order: 244 + order: 251 --- ## stream_supports_lock() diff --git a/docs/php/builtins/io/stream_wrapper_register.md b/docs/php/builtins/io/stream_wrapper_register.md index 44516e0db3..fe2f0ddc75 100644 --- a/docs/php/builtins/io/stream_wrapper_register.md +++ b/docs/php/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register()" description: "Registers a URL wrapper implemented as a PHP class." sidebar: - order: 245 + order: 252 --- ## stream_wrapper_register() diff --git a/docs/php/builtins/io/stream_wrapper_restore.md b/docs/php/builtins/io/stream_wrapper_restore.md index 595cbed729..a7c52e4a13 100644 --- a/docs/php/builtins/io/stream_wrapper_restore.md +++ b/docs/php/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore()" description: "Restores a previously unregistered built-in wrapper." sidebar: - order: 246 + order: 253 --- ## stream_wrapper_restore() diff --git a/docs/php/builtins/io/stream_wrapper_unregister.md b/docs/php/builtins/io/stream_wrapper_unregister.md index 122004c063..4a9ab36699 100644 --- a/docs/php/builtins/io/stream_wrapper_unregister.md +++ b/docs/php/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister()" description: "Unregisters a previously registered URL wrapper." sidebar: - order: 247 + order: 254 --- ## stream_wrapper_unregister() diff --git a/docs/php/builtins/io/vfprintf.md b/docs/php/builtins/io/vfprintf.md index f10ebf776d..ba2a774201 100644 --- a/docs/php/builtins/io/vfprintf.md +++ b/docs/php/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf()" description: "Write a formatted string to a stream." sidebar: - order: 248 + order: 255 --- ## vfprintf() diff --git a/docs/php/builtins/json/json_decode.md b/docs/php/builtins/json/json_decode.md index ded966185b..cb8c126fba 100644 --- a/docs/php/builtins/json/json_decode.md +++ b/docs/php/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode()" description: "Decodes a JSON string." sidebar: - order: 249 + order: 256 --- ## json_decode() diff --git a/docs/php/builtins/json/json_encode.md b/docs/php/builtins/json/json_encode.md index 4da1a35ee2..cb9bca7b1d 100644 --- a/docs/php/builtins/json/json_encode.md +++ b/docs/php/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode()" description: "Returns the JSON representation of a value." sidebar: - order: 250 + order: 257 --- ## json_encode() diff --git a/docs/php/builtins/json/json_last_error.md b/docs/php/builtins/json/json_last_error.md index 189e240145..9da7abb36f 100644 --- a/docs/php/builtins/json/json_last_error.md +++ b/docs/php/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error()" description: "Returns the last error (if any) occurred during the last JSON encoding/decoding." sidebar: - order: 251 + order: 258 --- ## json_last_error() diff --git a/docs/php/builtins/json/json_last_error_msg.md b/docs/php/builtins/json/json_last_error_msg.md index 5ae149f70a..e5a6859871 100644 --- a/docs/php/builtins/json/json_last_error_msg.md +++ b/docs/php/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg()" description: "Returns the error string of the last json_encode() or json_decode() call." sidebar: - order: 252 + order: 259 --- ## json_last_error_msg() diff --git a/docs/php/builtins/json/json_validate.md b/docs/php/builtins/json/json_validate.md index dc5b98cfae..8c540a483b 100644 --- a/docs/php/builtins/json/json_validate.md +++ b/docs/php/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate()" description: "Checks if a string contains valid JSON." sidebar: - order: 253 + order: 260 --- ## json_validate() diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index 39b0a972a3..046790fc50 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -14,15 +14,21 @@ sidebar: | [`asin()`](./math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`atan()`](./math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`atan2()`](./math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`base_convert()`](./math/base_convert.md) | `(string $num, int $from_base, int $to_base): string` | `string` | ✓ | ✓ | +| [`bindec()`](./math/bindec.md) | `(string $binary_string): mixed` | `mixed` | ✓ | — | | [`ceil()`](./math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`clamp()`](./math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | | [`cos()`](./math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`cosh()`](./math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`decbin()`](./math/decbin.md) | `(int $num): string` | `string` | ✓ | — | +| [`dechex()`](./math/dechex.md) | `(int $num): string` | `string` | ✓ | — | +| [`decoct()`](./math/decoct.md) | `(int $num): string` | `string` | ✓ | — | | [`deg2rad()`](./math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`exp()`](./math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`fdiv()`](./math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | | [`floor()`](./math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`fmod()`](./math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hexdec()`](./math/hexdec.md) | `(string $hex_string): mixed` | `mixed` | ✓ | — | | [`hypot()`](./math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | | [`intdiv()`](./math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | | [`is_finite()`](./math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | @@ -34,12 +40,13 @@ sidebar: | [`max()`](./math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | | [`min()`](./math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | | [`mt_rand()`](./math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`octdec()`](./math/octdec.md) | `(string $octal_string): mixed` | `mixed` | ✓ | — | | [`pi()`](./math/pi.md) | `(): float` | `float` | ✓ | ✓ | | [`pow()`](./math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | | [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | | [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | -| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`round()`](./math/round.md) | `(float $num, int $precision = 0, int $mode = 1): float` | `float` | ✓ | ✓ | | [`sin()`](./math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | | [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | diff --git a/docs/php/builtins/math/abs.md b/docs/php/builtins/math/abs.md index c807d99b89..8c5d746e05 100644 --- a/docs/php/builtins/math/abs.md +++ b/docs/php/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs()" description: "Absolute value." sidebar: - order: 254 + order: 261 --- ## abs() diff --git a/docs/php/builtins/math/acos.md b/docs/php/builtins/math/acos.md index 79d92b3637..3405ffe77d 100644 --- a/docs/php/builtins/math/acos.md +++ b/docs/php/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos()" description: "Returns the arccosine of a number in radians." sidebar: - order: 255 + order: 262 --- ## acos() diff --git a/docs/php/builtins/math/asin.md b/docs/php/builtins/math/asin.md index f9b3a58a37..559980986e 100644 --- a/docs/php/builtins/math/asin.md +++ b/docs/php/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin()" description: "Returns the arcsine of a number in radians." sidebar: - order: 256 + order: 263 --- ## asin() diff --git a/docs/php/builtins/math/atan.md b/docs/php/builtins/math/atan.md index b49ab19326..e2e4f65d06 100644 --- a/docs/php/builtins/math/atan.md +++ b/docs/php/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan()" description: "Returns the arctangent of a number in radians." sidebar: - order: 257 + order: 264 --- ## atan() diff --git a/docs/php/builtins/math/atan2.md b/docs/php/builtins/math/atan2.md index f204f10d95..75f7635044 100644 --- a/docs/php/builtins/math/atan2.md +++ b/docs/php/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2()" description: "Returns the arc tangent of two variables." sidebar: - order: 258 + order: 265 --- ## atan2() diff --git a/docs/php/builtins/math/base_convert.md b/docs/php/builtins/math/base_convert.md new file mode 100644 index 0000000000..40f3a3ad90 --- /dev/null +++ b/docs/php/builtins/math/base_convert.md @@ -0,0 +1,38 @@ +--- +title: "base_convert()" +description: "Converts a number between two arbitrary bases from 2 to 36." +sidebar: + order: 266 +--- + +## base_convert() + +```php +function base_convert(string $num, int $from_base, int $to_base): string +``` + +Converts a number between two arbitrary bases from 2 to 36. + +**Parameters**: +- `$num` (`string`) +- `$from_base` (`int`) +- `$to_base` (`int`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `base_convert` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/base_convert.md). diff --git a/docs/php/builtins/math/bindec.md b/docs/php/builtins/math/bindec.md new file mode 100644 index 0000000000..16727cdeda --- /dev/null +++ b/docs/php/builtins/math/bindec.md @@ -0,0 +1,36 @@ +--- +title: "bindec()" +description: "Converts a binary string to its decimal number." +sidebar: + order: 267 +--- + +## bindec() + +```php +function bindec(string $binary_string): mixed +``` + +Converts a binary string to its decimal number. + +**Parameters**: +- `$binary_string` (`string`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `bindec` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/bindec.md). diff --git a/docs/php/builtins/math/ceil.md b/docs/php/builtins/math/ceil.md index 84e4488e66..459c17a443 100644 --- a/docs/php/builtins/math/ceil.md +++ b/docs/php/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil()" description: "Rounds a number up to the nearest integer." sidebar: - order: 259 + order: 268 --- ## ceil() diff --git a/docs/php/builtins/math/clamp.md b/docs/php/builtins/math/clamp.md index 7630f6ac29..dea05c64c2 100644 --- a/docs/php/builtins/math/clamp.md +++ b/docs/php/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp()" description: "Clamps a value to be within a specified range." sidebar: - order: 260 + order: 269 --- ## clamp() diff --git a/docs/php/builtins/math/cos.md b/docs/php/builtins/math/cos.md index 0835b3712f..1240941247 100644 --- a/docs/php/builtins/math/cos.md +++ b/docs/php/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos()" description: "Returns the cosine of a number (radians)." sidebar: - order: 261 + order: 270 --- ## cos() diff --git a/docs/php/builtins/math/cosh.md b/docs/php/builtins/math/cosh.md index 91d79ebf9b..9bd8de2234 100644 --- a/docs/php/builtins/math/cosh.md +++ b/docs/php/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh()" description: "Returns the hyperbolic cosine of a number." sidebar: - order: 262 + order: 271 --- ## cosh() diff --git a/docs/php/builtins/math/decbin.md b/docs/php/builtins/math/decbin.md new file mode 100644 index 0000000000..fefef6f240 --- /dev/null +++ b/docs/php/builtins/math/decbin.md @@ -0,0 +1,36 @@ +--- +title: "decbin()" +description: "Converts an integer to its binary string representation." +sidebar: + order: 272 +--- + +## decbin() + +```php +function decbin(int $num): string +``` + +Converts an integer to its binary string representation. + +**Parameters**: +- `$num` (`int`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `decbin` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/decbin.md). diff --git a/docs/php/builtins/math/dechex.md b/docs/php/builtins/math/dechex.md new file mode 100644 index 0000000000..2b3a385ab2 --- /dev/null +++ b/docs/php/builtins/math/dechex.md @@ -0,0 +1,36 @@ +--- +title: "dechex()" +description: "Converts an integer to its hexadecimal string representation." +sidebar: + order: 273 +--- + +## dechex() + +```php +function dechex(int $num): string +``` + +Converts an integer to its hexadecimal string representation. + +**Parameters**: +- `$num` (`int`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `dechex` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/dechex.md). diff --git a/docs/php/builtins/math/decoct.md b/docs/php/builtins/math/decoct.md new file mode 100644 index 0000000000..6f6711a1a1 --- /dev/null +++ b/docs/php/builtins/math/decoct.md @@ -0,0 +1,36 @@ +--- +title: "decoct()" +description: "Converts an integer to its octal string representation." +sidebar: + order: 274 +--- + +## decoct() + +```php +function decoct(int $num): string +``` + +Converts an integer to its octal string representation. + +**Parameters**: +- `$num` (`int`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `decoct` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/decoct.md). diff --git a/docs/php/builtins/math/deg2rad.md b/docs/php/builtins/math/deg2rad.md index 86dea1d899..aec25241eb 100644 --- a/docs/php/builtins/math/deg2rad.md +++ b/docs/php/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad()" description: "Converts a degree value to radians." sidebar: - order: 263 + order: 275 --- ## deg2rad() diff --git a/docs/php/builtins/math/exp.md b/docs/php/builtins/math/exp.md index b36d27b318..f6ae7b5439 100644 --- a/docs/php/builtins/math/exp.md +++ b/docs/php/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp()" description: "Returns e raised to the power of a number." sidebar: - order: 264 + order: 276 --- ## exp() diff --git a/docs/php/builtins/math/fdiv.md b/docs/php/builtins/math/fdiv.md index ff590ea23b..33846ce301 100644 --- a/docs/php/builtins/math/fdiv.md +++ b/docs/php/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv()" description: "Divides two numbers, according to IEEE 754." sidebar: - order: 265 + order: 277 --- ## fdiv() diff --git a/docs/php/builtins/math/floor.md b/docs/php/builtins/math/floor.md index 37759525ab..473cfbfa99 100644 --- a/docs/php/builtins/math/floor.md +++ b/docs/php/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor()" description: "Rounds a number down to the nearest integer." sidebar: - order: 266 + order: 278 --- ## floor() diff --git a/docs/php/builtins/math/fmod.md b/docs/php/builtins/math/fmod.md index c2a7a5f8dc..e5b6729f21 100644 --- a/docs/php/builtins/math/fmod.md +++ b/docs/php/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod()" description: "Returns the floating point remainder of the division of the arguments." sidebar: - order: 267 + order: 279 --- ## fmod() diff --git a/docs/php/builtins/math/hexdec.md b/docs/php/builtins/math/hexdec.md new file mode 100644 index 0000000000..6426b786d8 --- /dev/null +++ b/docs/php/builtins/math/hexdec.md @@ -0,0 +1,36 @@ +--- +title: "hexdec()" +description: "Converts a hexadecimal string to its decimal number." +sidebar: + order: 280 +--- + +## hexdec() + +```php +function hexdec(string $hex_string): mixed +``` + +Converts a hexadecimal string to its decimal number. + +**Parameters**: +- `$hex_string` (`string`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `hexdec` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/hexdec.md). diff --git a/docs/php/builtins/math/hypot.md b/docs/php/builtins/math/hypot.md index 4608ce1d8f..c951acee32 100644 --- a/docs/php/builtins/math/hypot.md +++ b/docs/php/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot()" description: "Calculates the length of the hypotenuse of a right-angle triangle." sidebar: - order: 268 + order: 281 --- ## hypot() diff --git a/docs/php/builtins/math/intdiv.md b/docs/php/builtins/math/intdiv.md index 8af55c988e..15d786d40c 100644 --- a/docs/php/builtins/math/intdiv.md +++ b/docs/php/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv()" description: "Integer division." sidebar: - order: 269 + order: 282 --- ## intdiv() diff --git a/docs/php/builtins/math/is_finite.md b/docs/php/builtins/math/is_finite.md index e94d925119..f1a7e17060 100644 --- a/docs/php/builtins/math/is_finite.md +++ b/docs/php/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite()" description: "Checks whether a float is finite." sidebar: - order: 270 + order: 283 --- ## is_finite() diff --git a/docs/php/builtins/math/is_infinite.md b/docs/php/builtins/math/is_infinite.md index 877aa67e9b..4872fbd50c 100644 --- a/docs/php/builtins/math/is_infinite.md +++ b/docs/php/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite()" description: "Checks whether a float is infinite." sidebar: - order: 271 + order: 284 --- ## is_infinite() diff --git a/docs/php/builtins/math/is_nan.md b/docs/php/builtins/math/is_nan.md index ff041b0d98..fe9a6ccfe0 100644 --- a/docs/php/builtins/math/is_nan.md +++ b/docs/php/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan()" description: "Checks whether a float is NAN." sidebar: - order: 272 + order: 285 --- ## is_nan() diff --git a/docs/php/builtins/math/log.md b/docs/php/builtins/math/log.md index 67c5be4d89..284861d8ac 100644 --- a/docs/php/builtins/math/log.md +++ b/docs/php/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log()" description: "Natural logarithm." sidebar: - order: 273 + order: 286 --- ## log() diff --git a/docs/php/builtins/math/log10.md b/docs/php/builtins/math/log10.md index 2f27f94e42..5d49dcc259 100644 --- a/docs/php/builtins/math/log10.md +++ b/docs/php/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10()" description: "Returns the base-10 logarithm of a number." sidebar: - order: 274 + order: 287 --- ## log10() diff --git a/docs/php/builtins/math/log2.md b/docs/php/builtins/math/log2.md index 543d6f3941..9be338cb17 100644 --- a/docs/php/builtins/math/log2.md +++ b/docs/php/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2()" description: "Returns the base-2 logarithm of a number." sidebar: - order: 275 + order: 288 --- ## log2() diff --git a/docs/php/builtins/math/max.md b/docs/php/builtins/math/max.md index 0a9f51f099..010029332d 100644 --- a/docs/php/builtins/math/max.md +++ b/docs/php/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max()" description: "Find highest value." sidebar: - order: 276 + order: 289 --- ## max() diff --git a/docs/php/builtins/math/min.md b/docs/php/builtins/math/min.md index 83d393554a..2cfa169f3c 100644 --- a/docs/php/builtins/math/min.md +++ b/docs/php/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min()" description: "Find lowest value." sidebar: - order: 277 + order: 290 --- ## min() diff --git a/docs/php/builtins/math/mt_rand.md b/docs/php/builtins/math/mt_rand.md index 5b3c44eae2..6e16245960 100644 --- a/docs/php/builtins/math/mt_rand.md +++ b/docs/php/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand()" description: "Generate a random value via the Mersenne Twister Random Number Generator." sidebar: - order: 278 + order: 291 --- ## mt_rand() diff --git a/docs/php/builtins/math/octdec.md b/docs/php/builtins/math/octdec.md new file mode 100644 index 0000000000..0f2b9d7ba7 --- /dev/null +++ b/docs/php/builtins/math/octdec.md @@ -0,0 +1,36 @@ +--- +title: "octdec()" +description: "Converts a octal string to its decimal number." +sidebar: + order: 292 +--- + +## octdec() + +```php +function octdec(string $octal_string): mixed +``` + +Converts a octal string to its decimal number. + +**Parameters**: +- `$octal_string` (`string`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `octdec` is implemented in the compiler, see [the internals page](../../../internals/builtins/math/octdec.md). diff --git a/docs/php/builtins/math/pi.md b/docs/php/builtins/math/pi.md index 7bf0107805..2e77010acb 100644 --- a/docs/php/builtins/math/pi.md +++ b/docs/php/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi()" description: "Gets value of pi." sidebar: - order: 279 + order: 293 --- ## pi() diff --git a/docs/php/builtins/math/pow.md b/docs/php/builtins/math/pow.md index ad34f26e00..d32a3603f1 100644 --- a/docs/php/builtins/math/pow.md +++ b/docs/php/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow()" description: "Exponential expression." sidebar: - order: 280 + order: 294 --- ## pow() diff --git a/docs/php/builtins/math/rad2deg.md b/docs/php/builtins/math/rad2deg.md index 507784f090..cb98136603 100644 --- a/docs/php/builtins/math/rad2deg.md +++ b/docs/php/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg()" description: "Converts a radian value to degrees." sidebar: - order: 281 + order: 295 --- ## rad2deg() diff --git a/docs/php/builtins/math/rand.md b/docs/php/builtins/math/rand.md index 7e74e018ca..9cf3c1aa47 100644 --- a/docs/php/builtins/math/rand.md +++ b/docs/php/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand()" description: "Generate a random integer." sidebar: - order: 282 + order: 296 --- ## rand() diff --git a/docs/php/builtins/math/random_int.md b/docs/php/builtins/math/random_int.md index 567ee28753..d5dfb2d587 100644 --- a/docs/php/builtins/math/random_int.md +++ b/docs/php/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int()" description: "Get a cryptographically secure, uniformly selected integer." sidebar: - order: 283 + order: 297 --- ## random_int() diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index c0240ef99d..575d3f9be9 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -2,13 +2,13 @@ title: "round()" description: "Rounds a float." sidebar: - order: 284 + order: 298 --- ## round() ```php -function round(float $num, int $precision = 0): float +function round(float $num, int $precision = 0, int $mode = 1): float ``` Rounds a float. @@ -16,6 +16,7 @@ Rounds a float. **Parameters**: - `$num` (`float`) - `$precision` (`int`), default `0`, optional +- `$mode` (`int`), default `1`, optional **Returns**: `float` diff --git a/docs/php/builtins/math/sin.md b/docs/php/builtins/math/sin.md index babbb6ed17..cec1272f85 100644 --- a/docs/php/builtins/math/sin.md +++ b/docs/php/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin()" description: "Returns the sine of a number (radians)." sidebar: - order: 285 + order: 299 --- ## sin() diff --git a/docs/php/builtins/math/sinh.md b/docs/php/builtins/math/sinh.md index d75da63b73..334246d697 100644 --- a/docs/php/builtins/math/sinh.md +++ b/docs/php/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh()" description: "Returns the hyperbolic sine of a number." sidebar: - order: 286 + order: 300 --- ## sinh() diff --git a/docs/php/builtins/math/sqrt.md b/docs/php/builtins/math/sqrt.md index 90678de8e1..196ea8b567 100644 --- a/docs/php/builtins/math/sqrt.md +++ b/docs/php/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt()" description: "Returns the square root of a number." sidebar: - order: 287 + order: 301 --- ## sqrt() diff --git a/docs/php/builtins/math/tan.md b/docs/php/builtins/math/tan.md index 1a01e9bb53..e91bb3feba 100644 --- a/docs/php/builtins/math/tan.md +++ b/docs/php/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan()" description: "Returns the tangent of a number (radians)." sidebar: - order: 288 + order: 302 --- ## tan() diff --git a/docs/php/builtins/math/tanh.md b/docs/php/builtins/math/tanh.md index 1d5474d4ac..cff5390411 100644 --- a/docs/php/builtins/math/tanh.md +++ b/docs/php/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh()" description: "Returns the hyperbolic tangent of a number." sidebar: - order: 289 + order: 303 --- ## tanh() diff --git a/docs/php/builtins/misc.md b/docs/php/builtins/misc.md index 08d00c0b33..a7d5a05dde 100644 --- a/docs/php/builtins/misc.md +++ b/docs/php/builtins/misc.md @@ -10,6 +10,7 @@ sidebar: | Function | Signature | Returns | AOT | eval() | |---|---|---|:-:|:-:| | [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`constant()`](./misc/constant.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | | [`define()`](./misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | | [`defined()`](./misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | | [`empty()`](./misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index b29c5cf732..fc5ce557a3 100644 --- a/docs/php/builtins/misc/buffer_new.md +++ b/docs/php/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new()" description: "buffer_new() — misc builtin supported by Elephc." sidebar: - order: 290 + order: 304 --- ## buffer_new() diff --git a/docs/php/builtins/misc/constant.md b/docs/php/builtins/misc/constant.md new file mode 100644 index 0000000000..301a466782 --- /dev/null +++ b/docs/php/builtins/misc/constant.md @@ -0,0 +1,36 @@ +--- +title: "constant()" +description: "Returns the value of a constant given its name." +sidebar: + order: 305 +--- + +## constant() + +```php +function constant(string $name): mixed +``` + +Returns the value of a constant given its name. + +**Parameters**: +- `$name` (`string`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/constant.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/constant.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `constant` is implemented in the compiler, see [the internals page](../../../internals/builtins/misc/constant.md). diff --git a/docs/php/builtins/misc/define.md b/docs/php/builtins/misc/define.md index c4fcdf8c30..d7755f976b 100644 --- a/docs/php/builtins/misc/define.md +++ b/docs/php/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define()" description: "Defines a named constant at runtime." sidebar: - order: 291 + order: 306 --- ## define() diff --git a/docs/php/builtins/misc/defined.md b/docs/php/builtins/misc/defined.md index d0788880b0..019d080267 100644 --- a/docs/php/builtins/misc/defined.md +++ b/docs/php/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined()" description: "Checks whether a given named constant exists." sidebar: - order: 292 + order: 307 --- ## defined() diff --git a/docs/php/builtins/misc/empty.md b/docs/php/builtins/misc/empty.md index ee958199ad..c016c63d34 100644 --- a/docs/php/builtins/misc/empty.md +++ b/docs/php/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty()" description: "Determines whether a variable is considered empty." sidebar: - order: 293 + order: 308 --- ## empty() diff --git a/docs/php/builtins/misc/extension_loaded.md b/docs/php/builtins/misc/extension_loaded.md index 93b4e2d02d..25a03e61ef 100644 --- a/docs/php/builtins/misc/extension_loaded.md +++ b/docs/php/builtins/misc/extension_loaded.md @@ -2,7 +2,7 @@ title: "extension_loaded()" description: "Checks whether a named PHP extension is loaded." sidebar: - order: 294 + order: 309 --- ## extension_loaded() diff --git a/docs/php/builtins/misc/get_loaded_extensions.md b/docs/php/builtins/misc/get_loaded_extensions.md index 9fb207946b..87d2c1c80e 100644 --- a/docs/php/builtins/misc/get_loaded_extensions.md +++ b/docs/php/builtins/misc/get_loaded_extensions.md @@ -2,7 +2,7 @@ title: "get_loaded_extensions()" description: "Returns an array with the names of all loaded modules." sidebar: - order: 295 + order: 310 --- ## get_loaded_extensions() diff --git a/docs/php/builtins/misc/header.md b/docs/php/builtins/misc/header.md index ca697f041d..fb645e4f55 100644 --- a/docs/php/builtins/misc/header.md +++ b/docs/php/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header()" description: "Sends a raw HTTP header." sidebar: - order: 296 + order: 311 --- ## header() diff --git a/docs/php/builtins/misc/http_response_code.md b/docs/php/builtins/misc/http_response_code.md index 735c577916..e842aadd46 100644 --- a/docs/php/builtins/misc/http_response_code.md +++ b/docs/php/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code()" description: "Gets or sets the HTTP response code." sidebar: - order: 297 + order: 312 --- ## http_response_code() diff --git a/docs/php/builtins/misc/isset.md b/docs/php/builtins/misc/isset.md index 24924c2200..9848cf0aba 100644 --- a/docs/php/builtins/misc/isset.md +++ b/docs/php/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset()" description: "Determines whether a variable is set and is not null." sidebar: - order: 298 + order: 313 --- ## isset() diff --git a/docs/php/builtins/misc/php_uname.md b/docs/php/builtins/misc/php_uname.md index 24a9eadb33..87caf326d3 100644 --- a/docs/php/builtins/misc/php_uname.md +++ b/docs/php/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname()" description: "Returns information about the operating system PHP is running on." sidebar: - order: 299 + order: 314 --- ## php_uname() diff --git a/docs/php/builtins/misc/phpversion.md b/docs/php/builtins/misc/phpversion.md index 6ffbc7516e..9b4f9b47cb 100644 --- a/docs/php/builtins/misc/phpversion.md +++ b/docs/php/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion()" description: "Returns the targeted PHP language version, or one extension's version." sidebar: - order: 300 + order: 315 --- ## phpversion() diff --git a/docs/php/builtins/misc/print_r.md b/docs/php/builtins/misc/print_r.md index d68d83be97..2d06099728 100644 --- a/docs/php/builtins/misc/print_r.md +++ b/docs/php/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r()" description: "Prints human-readable information about a variable." sidebar: - order: 301 + order: 316 --- ## print_r() diff --git a/docs/php/builtins/misc/serialize.md b/docs/php/builtins/misc/serialize.md index c27bd58a17..67aa837056 100644 --- a/docs/php/builtins/misc/serialize.md +++ b/docs/php/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize()" description: "Generates a storable representation of a value." sidebar: - order: 302 + order: 317 --- ## serialize() diff --git a/docs/php/builtins/misc/unserialize.md b/docs/php/builtins/misc/unserialize.md index a398603fa7..6cbf1d2d4b 100644 --- a/docs/php/builtins/misc/unserialize.md +++ b/docs/php/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize()" description: "Creates a PHP value from a stored representation." sidebar: - order: 303 + order: 318 --- ## unserialize() diff --git a/docs/php/builtins/misc/unset.md b/docs/php/builtins/misc/unset.md index 2d45211f75..f82b43b902 100644 --- a/docs/php/builtins/misc/unset.md +++ b/docs/php/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset()" description: "Unsets the given variables." sidebar: - order: 304 + order: 319 --- ## unset() diff --git a/docs/php/builtins/misc/var_dump.md b/docs/php/builtins/misc/var_dump.md index 199cce8c67..14f52bc741 100644 --- a/docs/php/builtins/misc/var_dump.md +++ b/docs/php/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump()" description: "Dumps information about a variable, including its type and value." sidebar: - order: 305 + order: 320 --- ## var_dump() diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index a4da27ec18..374ed68398 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr()" description: "Returns a raw pointer to the given variable." sidebar: - order: 306 + order: 321 --- ## ptr() diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index b379bf3f2e..20f9580066 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get()" description: "Reads one machine word through a raw pointer and returns it as an integer." sidebar: - order: 307 + order: 322 --- ## ptr_get() diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 305c239591..7582da0844 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null()" description: "Returns true if the pointer is null." sidebar: - order: 308 + order: 323 --- ## ptr_is_null() diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index 035311d98a..9abe74520b 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null()" description: "Returns a null raw pointer." sidebar: - order: 309 + order: 324 --- ## ptr_null() diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index da1be5ee50..6417eb4533 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset()" description: "Returns a new pointer offset from the given pointer by the given byte count." sidebar: - order: 310 + order: 325 --- ## ptr_offset() diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index 00893074c4..80403c5a09 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16()" description: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer." sidebar: - order: 311 + order: 326 --- ## ptr_read16() diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index 66e4997bd4..0216ce5a23 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32()" description: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer." sidebar: - order: 312 + order: 327 --- ## ptr_read32() diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index b3297afff1..dad6573f16 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8()" description: "Reads one unsigned byte through a raw pointer and returns it as an integer." sidebar: - order: 313 + order: 328 --- ## ptr_read8() diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index 4b855e0524..a8572b3a3a 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string()" description: "Copies raw bytes from a pointer into a PHP string of the given length." sidebar: - order: 314 + order: 329 --- ## ptr_read_string() diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index 37fd96a0ea..8666b2e75c 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set()" description: "Writes one machine word through a raw pointer." sidebar: - order: 315 + order: 330 --- ## ptr_set() diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index 3b8207532b..ca23990ae6 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof()" description: "Returns the byte size of the named pointer target type." sidebar: - order: 316 + order: 331 --- ## ptr_sizeof() diff --git a/docs/php/builtins/pointer/ptr_write16.md b/docs/php/builtins/pointer/ptr_write16.md index 072a9b6c6c..d7b77dcc0d 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16()" description: "Writes one 16-bit word through a raw pointer." sidebar: - order: 317 + order: 332 --- ## ptr_write16() diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index 1c71b440d0..dc25608f5a 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32()" description: "Writes one 32-bit word through a raw pointer." sidebar: - order: 318 + order: 333 --- ## ptr_write32() diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index 83bbd09323..a98b8ee33e 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8()" description: "Writes one byte through a raw pointer." sidebar: - order: 319 + order: 334 --- ## ptr_write8() diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index ae92e639b4..373e68bb08 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string()" description: "Copies PHP string bytes into raw memory at the given pointer." sidebar: - order: 320 + order: 335 --- ## ptr_write_string() diff --git a/docs/php/builtins/pointer/zval_free.md b/docs/php/builtins/pointer/zval_free.md index a4e17ca8f1..37608f9201 100644 --- a/docs/php/builtins/pointer/zval_free.md +++ b/docs/php/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free()" description: "Frees a PHP zval pointer allocated by `zval_pack`." sidebar: - order: 321 + order: 336 --- ## zval_free() diff --git a/docs/php/builtins/pointer/zval_pack.md b/docs/php/builtins/pointer/zval_pack.md index 0c0a0813f1..5490bfac74 100644 --- a/docs/php/builtins/pointer/zval_pack.md +++ b/docs/php/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack()" description: "Packs an elephc runtime value into a heap-allocated PHP zval pointer." sidebar: - order: 322 + order: 337 --- ## zval_pack() diff --git a/docs/php/builtins/pointer/zval_type.md b/docs/php/builtins/pointer/zval_type.md index 8e6d501874..ddf1e947b2 100644 --- a/docs/php/builtins/pointer/zval_type.md +++ b/docs/php/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type()" description: "Returns the PHP zval type byte for a zval pointer." sidebar: - order: 323 + order: 338 --- ## zval_type() diff --git a/docs/php/builtins/pointer/zval_unpack.md b/docs/php/builtins/pointer/zval_unpack.md index c264a99cd2..022fbd33f3 100644 --- a/docs/php/builtins/pointer/zval_unpack.md +++ b/docs/php/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack()" description: "Unpacks a PHP zval pointer into an owned elephc Mixed value." sidebar: - order: 324 + order: 339 --- ## zval_unpack() diff --git a/docs/php/builtins/process/die.md b/docs/php/builtins/process/die.md index 9c61ddfe33..b0fd8e0e17 100644 --- a/docs/php/builtins/process/die.md +++ b/docs/php/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die()" description: "die() — process builtin supported by Elephc." sidebar: - order: 325 + order: 340 --- ## die() diff --git a/docs/php/builtins/process/exec.md b/docs/php/builtins/process/exec.md index 007ee349fd..1a8f91c23b 100644 --- a/docs/php/builtins/process/exec.md +++ b/docs/php/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec()" description: "Executes an external program and returns the last line of output." sidebar: - order: 326 + order: 341 --- ## exec() diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 91b89334a7..ec4425915c 100644 --- a/docs/php/builtins/process/exit.md +++ b/docs/php/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit()" description: "exit() — process builtin supported by Elephc." sidebar: - order: 327 + order: 342 --- ## exit() diff --git a/docs/php/builtins/process/passthru.md b/docs/php/builtins/process/passthru.md index f536aaf3fe..df225235a7 100644 --- a/docs/php/builtins/process/passthru.md +++ b/docs/php/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru()" description: "Executes an external program and passes its output directly." sidebar: - order: 328 + order: 343 --- ## passthru() diff --git a/docs/php/builtins/process/pclose.md b/docs/php/builtins/process/pclose.md index b9c3f50d40..66e65ad2b0 100644 --- a/docs/php/builtins/process/pclose.md +++ b/docs/php/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose()" description: "Closes process file pointer." sidebar: - order: 329 + order: 344 --- ## pclose() diff --git a/docs/php/builtins/process/popen.md b/docs/php/builtins/process/popen.md index bfb0f516e4..66520305bd 100644 --- a/docs/php/builtins/process/popen.md +++ b/docs/php/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen()" description: "Opens process file pointer." sidebar: - order: 330 + order: 345 --- ## popen() diff --git a/docs/php/builtins/process/readline.md b/docs/php/builtins/process/readline.md index 6fdef59259..f29c6f2990 100644 --- a/docs/php/builtins/process/readline.md +++ b/docs/php/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline()" description: "Reads a line from the user's terminal." sidebar: - order: 331 + order: 346 --- ## readline() diff --git a/docs/php/builtins/process/shell_exec.md b/docs/php/builtins/process/shell_exec.md index 3a91d066f6..efabe14a3d 100644 --- a/docs/php/builtins/process/shell_exec.md +++ b/docs/php/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec()" description: "Executes a command via the shell and returns the complete output as a string." sidebar: - order: 332 + order: 347 --- ## shell_exec() diff --git a/docs/php/builtins/process/sleep.md b/docs/php/builtins/process/sleep.md index bc357e782a..68ef33bb60 100644 --- a/docs/php/builtins/process/sleep.md +++ b/docs/php/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep()" description: "Delays execution for a number of seconds." sidebar: - order: 333 + order: 348 --- ## sleep() diff --git a/docs/php/builtins/process/system.md b/docs/php/builtins/process/system.md index 7066e14352..1b61a77130 100644 --- a/docs/php/builtins/process/system.md +++ b/docs/php/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system()" description: "Executes an external program and displays the output." sidebar: - order: 334 + order: 349 --- ## system() diff --git a/docs/php/builtins/process/usleep.md b/docs/php/builtins/process/usleep.md index 6e8d759a27..4a98fbba83 100644 --- a/docs/php/builtins/process/usleep.md +++ b/docs/php/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep()" description: "Delays execution for a number of microseconds." sidebar: - order: 335 + order: 350 --- ## usleep() diff --git a/docs/php/builtins/regex/mb_ereg_match.md b/docs/php/builtins/regex/mb_ereg_match.md index 69fd5007a0..414e8f7c27 100644 --- a/docs/php/builtins/regex/mb_ereg_match.md +++ b/docs/php/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match()" description: "Tests whether a regex pattern matches the beginning of a string (multibyte)." sidebar: - order: 336 + order: 351 --- ## mb_ereg_match() diff --git a/docs/php/builtins/regex/preg_match.md b/docs/php/builtins/regex/preg_match.md index 9039b37dd1..34daf290f0 100644 --- a/docs/php/builtins/regex/preg_match.md +++ b/docs/php/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match()" description: "Performs a regular expression match." sidebar: - order: 337 + order: 352 --- ## preg_match() diff --git a/docs/php/builtins/regex/preg_match_all.md b/docs/php/builtins/regex/preg_match_all.md index 848286ec74..4edb338b03 100644 --- a/docs/php/builtins/regex/preg_match_all.md +++ b/docs/php/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all()" description: "Performs a global regular expression match and returns the number of matches." sidebar: - order: 338 + order: 353 --- ## preg_match_all() diff --git a/docs/php/builtins/regex/preg_replace.md b/docs/php/builtins/regex/preg_replace.md index 1ca6540bcd..5143920c49 100644 --- a/docs/php/builtins/regex/preg_replace.md +++ b/docs/php/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace()" description: "Performs a regular expression search and replace." sidebar: - order: 339 + order: 354 --- ## preg_replace() diff --git a/docs/php/builtins/regex/preg_replace_callback.md b/docs/php/builtins/regex/preg_replace_callback.md index 90b67e7da9..a24785d0e2 100644 --- a/docs/php/builtins/regex/preg_replace_callback.md +++ b/docs/php/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback()" description: "Performs a regular expression search and replace using a callback." sidebar: - order: 340 + order: 355 --- ## preg_replace_callback() diff --git a/docs/php/builtins/regex/preg_split.md b/docs/php/builtins/regex/preg_split.md index d00cce47f3..6b6248ba71 100644 --- a/docs/php/builtins/regex/preg_split.md +++ b/docs/php/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split()" description: "Splits a string by a regular expression." sidebar: - order: 341 + order: 356 --- ## preg_split() diff --git a/docs/php/builtins/spl/iterator_apply.md b/docs/php/builtins/spl/iterator_apply.md index 438e142a4f..74ddd7b435 100644 --- a/docs/php/builtins/spl/iterator_apply.md +++ b/docs/php/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply()" description: "Call a function for every element in an iterator." sidebar: - order: 342 + order: 357 --- ## iterator_apply() diff --git a/docs/php/builtins/spl/iterator_count.md b/docs/php/builtins/spl/iterator_count.md index 908ad1c05f..8cb4408e34 100644 --- a/docs/php/builtins/spl/iterator_count.md +++ b/docs/php/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count()" description: "Count the elements in an iterator." sidebar: - order: 343 + order: 358 --- ## iterator_count() diff --git a/docs/php/builtins/spl/iterator_to_array.md b/docs/php/builtins/spl/iterator_to_array.md index ac95948ebd..c74b465bb8 100644 --- a/docs/php/builtins/spl/iterator_to_array.md +++ b/docs/php/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array()" description: "Copy the iterator into an array." sidebar: - order: 344 + order: 359 --- ## iterator_to_array() diff --git a/docs/php/builtins/spl/spl_autoload.md b/docs/php/builtins/spl/spl_autoload.md index 0d2019263f..49a65e7ae9 100644 --- a/docs/php/builtins/spl/spl_autoload.md +++ b/docs/php/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload()" description: "Default implementation for __autoload()." sidebar: - order: 345 + order: 360 --- ## spl_autoload() diff --git a/docs/php/builtins/spl/spl_autoload_call.md b/docs/php/builtins/spl/spl_autoload_call.md index c1cbfbbb69..d7de4fe301 100644 --- a/docs/php/builtins/spl/spl_autoload_call.md +++ b/docs/php/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call()" description: "Try all registered __autoload() functions to load the requested class." sidebar: - order: 346 + order: 361 --- ## spl_autoload_call() diff --git a/docs/php/builtins/spl/spl_autoload_extensions.md b/docs/php/builtins/spl/spl_autoload_extensions.md index 4f8f2b702e..c729d0fd1c 100644 --- a/docs/php/builtins/spl/spl_autoload_extensions.md +++ b/docs/php/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions()" description: "Register and return default file extensions for spl_autoload." sidebar: - order: 347 + order: 362 --- ## spl_autoload_extensions() diff --git a/docs/php/builtins/spl/spl_autoload_functions.md b/docs/php/builtins/spl/spl_autoload_functions.md index b4e5676e76..0c68d1c73c 100644 --- a/docs/php/builtins/spl/spl_autoload_functions.md +++ b/docs/php/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions()" description: "Return all registered __autoload() functions." sidebar: - order: 348 + order: 363 --- ## spl_autoload_functions() diff --git a/docs/php/builtins/spl/spl_autoload_register.md b/docs/php/builtins/spl/spl_autoload_register.md index 0994b31e5b..866214c532 100644 --- a/docs/php/builtins/spl/spl_autoload_register.md +++ b/docs/php/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register()" description: "Register given function as __autoload() implementation." sidebar: - order: 349 + order: 364 --- ## spl_autoload_register() diff --git a/docs/php/builtins/spl/spl_autoload_unregister.md b/docs/php/builtins/spl/spl_autoload_unregister.md index dc9db6fc74..59d99313de 100644 --- a/docs/php/builtins/spl/spl_autoload_unregister.md +++ b/docs/php/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister()" description: "Unregister given function as __autoload() implementation." sidebar: - order: 350 + order: 365 --- ## spl_autoload_unregister() diff --git a/docs/php/builtins/spl/spl_classes.md b/docs/php/builtins/spl/spl_classes.md index 09872ed9c0..e6b4421755 100644 --- a/docs/php/builtins/spl/spl_classes.md +++ b/docs/php/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes()" description: "Return available SPL classes." sidebar: - order: 351 + order: 366 --- ## spl_classes() diff --git a/docs/php/builtins/spl/spl_object_hash.md b/docs/php/builtins/spl/spl_object_hash.md index c9e0e14f17..fdfedaf2ad 100644 --- a/docs/php/builtins/spl/spl_object_hash.md +++ b/docs/php/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash()" description: "Return hash id for given object." sidebar: - order: 352 + order: 367 --- ## spl_object_hash() diff --git a/docs/php/builtins/spl/spl_object_id.md b/docs/php/builtins/spl/spl_object_id.md index 211fdf5d78..b404a35a63 100644 --- a/docs/php/builtins/spl/spl_object_id.md +++ b/docs/php/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id()" description: "Return the integer object handle for given object." sidebar: - order: 353 + order: 368 --- ## spl_object_id() diff --git a/docs/php/builtins/streams/fsockopen.md b/docs/php/builtins/streams/fsockopen.md index 393b8442f0..6d53f53f8f 100644 --- a/docs/php/builtins/streams/fsockopen.md +++ b/docs/php/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 354 + order: 369 --- ## fsockopen() diff --git a/docs/php/builtins/streams/pfsockopen.md b/docs/php/builtins/streams/pfsockopen.md index 9b7a054b3b..e3cdf50f58 100644 --- a/docs/php/builtins/streams/pfsockopen.md +++ b/docs/php/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen()" description: "Open persistent Internet or Unix domain socket connection." sidebar: - order: 355 + order: 370 --- ## pfsockopen() diff --git a/docs/php/builtins/streams/stream_bucket_append.md b/docs/php/builtins/streams/stream_bucket_append.md index b45c29d44d..48512c870b 100644 --- a/docs/php/builtins/streams/stream_bucket_append.md +++ b/docs/php/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append()" description: "Appends a bucket to the brigade." sidebar: - order: 356 + order: 371 --- ## stream_bucket_append() diff --git a/docs/php/builtins/streams/stream_bucket_prepend.md b/docs/php/builtins/streams/stream_bucket_prepend.md index 804b325d09..286a7a96e1 100644 --- a/docs/php/builtins/streams/stream_bucket_prepend.md +++ b/docs/php/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend()" description: "Prepends a bucket to the brigade." sidebar: - order: 357 + order: 372 --- ## stream_bucket_prepend() diff --git a/docs/php/builtins/streams/stream_filter_append.md b/docs/php/builtins/streams/stream_filter_append.md index c45c52a5be..89f4eff7cc 100644 --- a/docs/php/builtins/streams/stream_filter_append.md +++ b/docs/php/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append()" description: "Attaches a filter to a stream." sidebar: - order: 358 + order: 373 --- ## stream_filter_append() diff --git a/docs/php/builtins/streams/stream_filter_prepend.md b/docs/php/builtins/streams/stream_filter_prepend.md index e16d67152d..a11c6fa9fe 100644 --- a/docs/php/builtins/streams/stream_filter_prepend.md +++ b/docs/php/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend()" description: "Attaches a filter to a stream (prepend)." sidebar: - order: 359 + order: 374 --- ## stream_filter_prepend() diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index 597300d27b..eb98dc56f0 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -10,11 +10,13 @@ sidebar: | Function | Signature | Returns | AOT | eval() | |---|---|---|:-:|:-:| | [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | -| [`base64_decode()`](./string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./string/base64_decode.md) | `(string $string, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | | [`base64_encode()`](./string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`bin2hex()`](./string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`chop()`](./string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | | [`chr()`](./string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`chunk_split()`](./string/chunk_split.md) | `(string $string, int $length = 76, string $separator = '\r\n'): string` | `string` | ✓ | ✓ | +| [`count_chars()`](./string/count_chars.md) | `(string $string, int $mode = 0): array|string` | `array|string` | ✓ | ✓ | | [`crc32()`](./string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | | [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | | [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | @@ -38,6 +40,7 @@ sidebar: | [`inet_ntop()`](./string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | | [`inet_pton()`](./string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | | [`ip2long()`](./string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`join()`](./string/join.md) | `(mixed $separator, mixed $array = null): string` | `string` | ✓ | — | | [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | | [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | @@ -48,6 +51,8 @@ sidebar: | [`ord()`](./string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | | [`parse_url()`](./string/parse_url.md) | `(string $url, int $component = -1): mixed` | `mixed` | ✓ | ✓ | | [`printf()`](./string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`quoted_printable_encode()`](./string/quoted_printable_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`quotemeta()`](./string/quotemeta.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rawurldecode()`](./string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rawurlencode()`](./string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`rtrim()`](./string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | @@ -62,17 +67,24 @@ sidebar: | [`str_replace()`](./string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | | [`str_split()`](./string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | | [`str_starts_with()`](./string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_word_count()`](./string/str_word_count.md) | `(string $string, int $format = 0, string $characters = null): array|int` | `array|int` | ✓ | ✓ | | [`strcasecmp()`](./string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | | [`strcmp()`](./string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripos()`](./string/stripos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`stripslashes()`](./string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`strlen()`](./string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strncasecmp()`](./string/strncasecmp.md) | `(string $string1, string $string2, int $length): int` | `int` | ✓ | — | +| [`strncmp()`](./string/strncmp.md) | `(string $string1, string $string2, int $length): int` | `int` | ✓ | — | | [`strpos()`](./string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strrev()`](./string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strripos()`](./string/strripos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strrpos()`](./string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | | [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): mixed` | `mixed` | ✓ | ✓ | | [`strtolower()`](./string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | | [`strtoupper()`](./string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtr()`](./string/strtr.md) | `(string $string, array|string $from, string $to = null): string` | `string` | ✓ | ✓ | | [`substr()`](./string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_count()`](./string/substr_count.md) | `(string $haystack, string $needle, int $offset = 0, mixed $length = null): int` | `int` | ✓ | — | | [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | | [`trim()`](./string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | | [`ucfirst()`](./string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/string/addslashes.md b/docs/php/builtins/string/addslashes.md index 70114528fd..6e3e5e9901 100644 --- a/docs/php/builtins/string/addslashes.md +++ b/docs/php/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes()" description: "Adds backslashes before characters that need to be escaped." sidebar: - order: 360 + order: 375 --- ## addslashes() diff --git a/docs/php/builtins/string/base64_decode.md b/docs/php/builtins/string/base64_decode.md index ece3d3dad3..fb4b6fbd95 100644 --- a/docs/php/builtins/string/base64_decode.md +++ b/docs/php/builtins/string/base64_decode.md @@ -2,21 +2,22 @@ title: "base64_decode()" description: "Decodes a Base64-encoded string back into its original data." sidebar: - order: 361 + order: 376 --- ## base64_decode() ```php -function base64_decode(string $string): string +function base64_decode(string $string, bool $strict = false): mixed ``` Decodes a Base64-encoded string back into its original data. **Parameters**: - `$string` (`string`) +- `$strict` (`bool`), default `false`, optional -**Returns**: `string` +**Returns**: `mixed` ## Availability diff --git a/docs/php/builtins/string/base64_encode.md b/docs/php/builtins/string/base64_encode.md index 13f80ff8ad..1acfd79bfe 100644 --- a/docs/php/builtins/string/base64_encode.md +++ b/docs/php/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode()" description: "Encodes binary data into a Base64 string." sidebar: - order: 362 + order: 377 --- ## base64_encode() diff --git a/docs/php/builtins/string/bin2hex.md b/docs/php/builtins/string/bin2hex.md index e209e2377b..bf68965178 100644 --- a/docs/php/builtins/string/bin2hex.md +++ b/docs/php/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex()" description: "Converts binary data into its hexadecimal string representation." sidebar: - order: 363 + order: 378 --- ## bin2hex() diff --git a/docs/php/builtins/string/chop.md b/docs/php/builtins/string/chop.md index 646ef6b885..be26c326db 100644 --- a/docs/php/builtins/string/chop.md +++ b/docs/php/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop()" description: "Alias of rtrim: strips whitespace (or other characters) from the end of a string." sidebar: - order: 364 + order: 379 --- ## chop() diff --git a/docs/php/builtins/string/chr.md b/docs/php/builtins/string/chr.md index d8666a1141..8731c696dd 100644 --- a/docs/php/builtins/string/chr.md +++ b/docs/php/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr()" description: "Returns a one-character string from the given byte code point." sidebar: - order: 365 + order: 380 --- ## chr() diff --git a/docs/php/builtins/string/chunk_split.md b/docs/php/builtins/string/chunk_split.md new file mode 100644 index 0000000000..bf379ce242 --- /dev/null +++ b/docs/php/builtins/string/chunk_split.md @@ -0,0 +1,38 @@ +--- +title: "chunk_split()" +description: "Splits a string into fixed-length chunks separated by a given string." +sidebar: + order: 381 +--- + +## chunk_split() + +```php +function chunk_split(string $string, int $length = 76, string $separator = '\r\n'): string +``` + +Splits a string into fixed-length chunks separated by a given string. + +**Parameters**: +- `$string` (`string`) +- `$length` (`int`), default `76`, optional +- `$separator` (`string`), default `'\r\n'`, optional + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `chunk_split` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/chunk_split.md). diff --git a/docs/php/builtins/string/count_chars.md b/docs/php/builtins/string/count_chars.md new file mode 100644 index 0000000000..2607f9b36f --- /dev/null +++ b/docs/php/builtins/string/count_chars.md @@ -0,0 +1,37 @@ +--- +title: "count_chars()" +description: "Returns byte-frequency information about a string as a tally or a byte list." +sidebar: + order: 382 +--- + +## count_chars() + +```php +function count_chars(string $string, int $mode = 0): array|string +``` + +Returns byte-frequency information about a string as a tally or a byte list. + +**Parameters**: +- `$string` (`string`) +- `$mode` (`int`), default `0`, optional + +**Returns**: `array|string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `count_chars` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/count_chars.md). diff --git a/docs/php/builtins/string/crc32.md b/docs/php/builtins/string/crc32.md index 80fabce011..8dd90063ed 100644 --- a/docs/php/builtins/string/crc32.md +++ b/docs/php/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32()" description: "Calculates the CRC32 polynomial of a string." sidebar: - order: 366 + order: 383 --- ## crc32() diff --git a/docs/php/builtins/string/explode.md b/docs/php/builtins/string/explode.md index a12a1876bc..d2711465e8 100644 --- a/docs/php/builtins/string/explode.md +++ b/docs/php/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode()" description: "Splits a string by a separator into an array of substrings." sidebar: - order: 367 + order: 384 --- ## explode() diff --git a/docs/php/builtins/string/grapheme_strrev.md b/docs/php/builtins/string/grapheme_strrev.md index 64f965f497..5114781c5d 100644 --- a/docs/php/builtins/string/grapheme_strrev.md +++ b/docs/php/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev()" description: "Reverses a string by grapheme cluster, returning false on failure." sidebar: - order: 368 + order: 385 --- ## grapheme_strrev() diff --git a/docs/php/builtins/string/gzcompress.md b/docs/php/builtins/string/gzcompress.md index 9a0a149fd1..cd5ee5e18f 100644 --- a/docs/php/builtins/string/gzcompress.md +++ b/docs/php/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress()" description: "Compress a string using the ZLIB data format." sidebar: - order: 369 + order: 386 --- ## gzcompress() diff --git a/docs/php/builtins/string/gzdeflate.md b/docs/php/builtins/string/gzdeflate.md index bbd4fee75a..ef2bf1a491 100644 --- a/docs/php/builtins/string/gzdeflate.md +++ b/docs/php/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate()" description: "Deflate a string using the DEFLATE data format." sidebar: - order: 370 + order: 387 --- ## gzdeflate() diff --git a/docs/php/builtins/string/gzinflate.md b/docs/php/builtins/string/gzinflate.md index 87cc1fe562..3f1205c74c 100644 --- a/docs/php/builtins/string/gzinflate.md +++ b/docs/php/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate()" description: "Inflate a deflated string." sidebar: - order: 371 + order: 388 --- ## gzinflate() diff --git a/docs/php/builtins/string/gzuncompress.md b/docs/php/builtins/string/gzuncompress.md index e5ec9e7090..e0c9bae893 100644 --- a/docs/php/builtins/string/gzuncompress.md +++ b/docs/php/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress()" description: "Uncompress a compressed string." sidebar: - order: 372 + order: 389 --- ## gzuncompress() diff --git a/docs/php/builtins/string/hash.md b/docs/php/builtins/string/hash.md index 17277dd16e..3874c6d589 100644 --- a/docs/php/builtins/string/hash.md +++ b/docs/php/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash()" description: "Generates a hash value using the given algorithm." sidebar: - order: 373 + order: 390 --- ## hash() diff --git a/docs/php/builtins/string/hash_algos.md b/docs/php/builtins/string/hash_algos.md index 03ede93832..2256996d89 100644 --- a/docs/php/builtins/string/hash_algos.md +++ b/docs/php/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos()" description: "Returns an array of supported hashing algorithm names." sidebar: - order: 374 + order: 391 --- ## hash_algos() diff --git a/docs/php/builtins/string/hash_copy.md b/docs/php/builtins/string/hash_copy.md index aedbd43ac5..051c353347 100644 --- a/docs/php/builtins/string/hash_copy.md +++ b/docs/php/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy()" description: "Clones an incremental hashing context into an independent HashContext object. Provided by the compiler-injected hash prelude in compiled code." sidebar: - order: 375 + order: 392 --- ## hash_copy() diff --git a/docs/php/builtins/string/hash_equals.md b/docs/php/builtins/string/hash_equals.md index c542e5aabb..6784030b6f 100644 --- a/docs/php/builtins/string/hash_equals.md +++ b/docs/php/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals()" description: "Compares two strings using a constant-time algorithm." sidebar: - order: 376 + order: 393 --- ## hash_equals() diff --git a/docs/php/builtins/string/hash_final.md b/docs/php/builtins/string/hash_final.md index caba19a0f4..e5131c66e1 100644 --- a/docs/php/builtins/string/hash_final.md +++ b/docs/php/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final()" description: "Finalizes an incremental hashing context and returns the digest (hex, or raw bytes when $binary). Provided by the compiler-injected hash prelude in compiled code." sidebar: - order: 377 + order: 394 --- ## hash_final() diff --git a/docs/php/builtins/string/hash_hmac.md b/docs/php/builtins/string/hash_hmac.md index 6bc459738b..911ab50593 100644 --- a/docs/php/builtins/string/hash_hmac.md +++ b/docs/php/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac()" description: "Generates a keyed hash value using the HMAC method." sidebar: - order: 378 + order: 395 --- ## hash_hmac() diff --git a/docs/php/builtins/string/hash_init.md b/docs/php/builtins/string/hash_init.md index 6523b476ca..1865e2568e 100644 --- a/docs/php/builtins/string/hash_init.md +++ b/docs/php/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init()" description: "Opens an incremental hashing context, returning a HashContext object. Provided by the compiler-injected hash prelude in compiled code; the eval interpreter still returns a resource." sidebar: - order: 379 + order: 396 --- ## hash_init() diff --git a/docs/php/builtins/string/hash_update.md b/docs/php/builtins/string/hash_update.md index 93e98279de..35ddae55cb 100644 --- a/docs/php/builtins/string/hash_update.md +++ b/docs/php/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update()" description: "Feeds data into an incremental hashing context. Provided by the compiler-injected hash prelude in compiled code." sidebar: - order: 380 + order: 397 --- ## hash_update() diff --git a/docs/php/builtins/string/hex2bin.md b/docs/php/builtins/string/hex2bin.md index 7314c8f07d..4c3a6c4fa4 100644 --- a/docs/php/builtins/string/hex2bin.md +++ b/docs/php/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin()" description: "Decodes a hexadecimal string back into its binary representation." sidebar: - order: 381 + order: 398 --- ## hex2bin() diff --git a/docs/php/builtins/string/html_entity_decode.md b/docs/php/builtins/string/html_entity_decode.md index bad2da2168..ba3c15f17d 100644 --- a/docs/php/builtins/string/html_entity_decode.md +++ b/docs/php/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode()" description: "Converts HTML entities in a string back into their corresponding characters." sidebar: - order: 382 + order: 399 --- ## html_entity_decode() diff --git a/docs/php/builtins/string/htmlentities.md b/docs/php/builtins/string/htmlentities.md index 4866a88502..21e9616275 100644 --- a/docs/php/builtins/string/htmlentities.md +++ b/docs/php/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities()" description: "Converts all applicable characters in a string into their HTML entities." sidebar: - order: 383 + order: 400 --- ## htmlentities() diff --git a/docs/php/builtins/string/htmlspecialchars.md b/docs/php/builtins/string/htmlspecialchars.md index d842cfea98..1f8d3ce762 100644 --- a/docs/php/builtins/string/htmlspecialchars.md +++ b/docs/php/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars()" description: "Converts the HTML special characters in a string into their entities." sidebar: - order: 384 + order: 401 --- ## htmlspecialchars() diff --git a/docs/php/builtins/string/implode.md b/docs/php/builtins/string/implode.md index dea707b6f6..def1f3cd42 100644 --- a/docs/php/builtins/string/implode.md +++ b/docs/php/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode()" description: "Joins array elements into a single string using a separator." sidebar: - order: 385 + order: 402 --- ## implode() diff --git a/docs/php/builtins/string/inet_ntop.md b/docs/php/builtins/string/inet_ntop.md index a30bc80d63..df925070ef 100644 --- a/docs/php/builtins/string/inet_ntop.md +++ b/docs/php/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop()" description: "Converts a packed internet address to a human-readable representation." sidebar: - order: 386 + order: 403 --- ## inet_ntop() diff --git a/docs/php/builtins/string/inet_pton.md b/docs/php/builtins/string/inet_pton.md index 552541e8f3..b2d0513576 100644 --- a/docs/php/builtins/string/inet_pton.md +++ b/docs/php/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton()" description: "Converts a human-readable IP address to its packed in_addr representation." sidebar: - order: 387 + order: 404 --- ## inet_pton() diff --git a/docs/php/builtins/string/ip2long.md b/docs/php/builtins/string/ip2long.md index c930af17e7..7ede266505 100644 --- a/docs/php/builtins/string/ip2long.md +++ b/docs/php/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long()" description: "Converts a string containing an IPv4 address into a long integer." sidebar: - order: 388 + order: 405 --- ## ip2long() diff --git a/docs/php/builtins/string/join.md b/docs/php/builtins/string/join.md new file mode 100644 index 0000000000..eeec5c8faf --- /dev/null +++ b/docs/php/builtins/string/join.md @@ -0,0 +1,37 @@ +--- +title: "join()" +description: "Joins array elements into a single string using a separator (alias of implode)." +sidebar: + order: 406 +--- + +## join() + +```php +function join(mixed $separator, mixed $array = null): string +``` + +Joins array elements into a single string using a separator (alias of implode). + +**Parameters**: +- `$separator` (`mixed`) +- `$array` (`mixed`), default `null`, optional + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `join` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/join.md). diff --git a/docs/php/builtins/string/lcfirst.md b/docs/php/builtins/string/lcfirst.md index a1358654f2..e313900d1c 100644 --- a/docs/php/builtins/string/lcfirst.md +++ b/docs/php/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst()" description: "Lowercases the first character of a string." sidebar: - order: 389 + order: 407 --- ## lcfirst() diff --git a/docs/php/builtins/string/long2ip.md b/docs/php/builtins/string/long2ip.md index 4df5005bc9..7fc8404c10 100644 --- a/docs/php/builtins/string/long2ip.md +++ b/docs/php/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip()" description: "Converts an IPv4 address from long integer to dotted string notation." sidebar: - order: 390 + order: 408 --- ## long2ip() diff --git a/docs/php/builtins/string/ltrim.md b/docs/php/builtins/string/ltrim.md index 120946db59..50ced15917 100644 --- a/docs/php/builtins/string/ltrim.md +++ b/docs/php/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim()" description: "Strips whitespace (or other characters) from the beginning of a string." sidebar: - order: 391 + order: 409 --- ## ltrim() diff --git a/docs/php/builtins/string/mb_strlen.md b/docs/php/builtins/string/mb_strlen.md index 01959aac71..d72b1d7597 100644 --- a/docs/php/builtins/string/mb_strlen.md +++ b/docs/php/builtins/string/mb_strlen.md @@ -2,7 +2,7 @@ title: "mb_strlen()" description: "Returns the character count of a string in the requested encoding." sidebar: - order: 392 + order: 410 --- ## mb_strlen() diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index cd0b081426..1d5e31ec8e 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5()" description: "Calculates the MD5 hash of a string." sidebar: - order: 393 + order: 411 --- ## md5() diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index c01a1b3409..0bfa967ea6 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br()" description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 394 + order: 412 --- ## nl2br() diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index 4678a9c5f3..c3a7cc9ea8 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format()" description: "Formats a number with grouped thousands." sidebar: - order: 395 + order: 413 --- ## number_format() diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index 89b09254f4..5b4f442e58 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord()" description: "Returns the ASCII value of the first character of a string." sidebar: - order: 396 + order: 414 --- ## ord() diff --git a/docs/php/builtins/string/parse_url.md b/docs/php/builtins/string/parse_url.md index 079388f2a4..4cdf3d16bf 100644 --- a/docs/php/builtins/string/parse_url.md +++ b/docs/php/builtins/string/parse_url.md @@ -2,7 +2,7 @@ title: "parse_url()" description: "Parses a URL and returns its components." sidebar: - order: 397 + order: 415 --- ## parse_url() diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index 979a37e542..ea77dfabcc 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf()" description: "Outputs a formatted string." sidebar: - order: 398 + order: 416 --- ## printf() diff --git a/docs/php/builtins/string/quoted_printable_encode.md b/docs/php/builtins/string/quoted_printable_encode.md new file mode 100644 index 0000000000..aa79263d54 --- /dev/null +++ b/docs/php/builtins/string/quoted_printable_encode.md @@ -0,0 +1,36 @@ +--- +title: "quoted_printable_encode()" +description: "Encodes a string with the MIME quoted-printable transfer encoding." +sidebar: + order: 417 +--- + +## quoted_printable_encode() + +```php +function quoted_printable_encode(string $string): string +``` + +Encodes a string with the MIME quoted-printable transfer encoding. + +**Parameters**: +- `$string` (`string`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `quoted_printable_encode` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/quoted_printable_encode.md). diff --git a/docs/php/builtins/string/quotemeta.md b/docs/php/builtins/string/quotemeta.md new file mode 100644 index 0000000000..23ca452ab0 --- /dev/null +++ b/docs/php/builtins/string/quotemeta.md @@ -0,0 +1,36 @@ +--- +title: "quotemeta()" +description: "Prefixes each regular-expression metacharacter in a string with a backslash." +sidebar: + order: 418 +--- + +## quotemeta() + +```php +function quotemeta(string $string): string +``` + +Prefixes each regular-expression metacharacter in a string with a backslash. + +**Parameters**: +- `$string` (`string`) + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `quotemeta` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/quotemeta.md). diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 30350ba17d..924edb0638 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode()" description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 399 + order: 419 --- ## rawurldecode() diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index 108f981f14..35354c235d 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode()" description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 400 + order: 420 --- ## rawurlencode() diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index c268245f4c..35eb73bbd2 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim()" description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 401 + order: 421 --- ## rtrim() diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index fe580a52a9..c0c6ada164 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1()" description: "Calculates the SHA-1 hash of a string." sidebar: - order: 402 + order: 422 --- ## sha1() diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index f7bb4feadd..ccbab61e56 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf()" description: "Returns a formatted string." sidebar: - order: 403 + order: 423 --- ## sprintf() diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 361472e9e4..e54c883e43 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf()" description: "Parses a string according to a format." sidebar: - order: 404 + order: 424 --- ## sscanf() diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index 73286677aa..fa3c18e18b 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains()" description: "Determines if a string contains a given substring." sidebar: - order: 405 + order: 425 --- ## str_contains() diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index d6d416c8c7..ba9e662a75 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with()" description: "Checks if a string ends with a given substring." sidebar: - order: 406 + order: 426 --- ## str_ends_with() diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index fae075a2f1..9abdc5dd09 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace()" description: "Case-insensitive version of str_replace()." sidebar: - order: 407 + order: 427 --- ## str_ireplace() diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 66073c4d85..c65cbab4a6 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad()" description: "Pads a string to a certain length with another string." sidebar: - order: 408 + order: 428 --- ## str_pad() diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index 8e5506e1f2..4ed00f90ba 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat()" description: "Repeats a string a given number of times." sidebar: - order: 409 + order: 429 --- ## str_repeat() diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index c44b7353d1..6ebd9d509a 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace()" description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 410 + order: 430 --- ## str_replace() diff --git a/docs/php/builtins/string/str_split.md b/docs/php/builtins/string/str_split.md index 98d24f6085..e503f71bf7 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split()" description: "Converts a string into an array of chunks of the given length." sidebar: - order: 411 + order: 431 --- ## str_split() diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index f5d4c260f9..84a11a41e8 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with()" description: "Checks if a string starts with a given substring." sidebar: - order: 412 + order: 432 --- ## str_starts_with() diff --git a/docs/php/builtins/string/str_word_count.md b/docs/php/builtins/string/str_word_count.md new file mode 100644 index 0000000000..fe96001aca --- /dev/null +++ b/docs/php/builtins/string/str_word_count.md @@ -0,0 +1,38 @@ +--- +title: "str_word_count()" +description: "Counts the words in a string, or returns them as a list or byte-offset map." +sidebar: + order: 433 +--- + +## str_word_count() + +```php +function str_word_count(string $string, int $format = 0, string $characters = null): array|int +``` + +Counts the words in a string, or returns them as a list or byte-offset map. + +**Parameters**: +- `$string` (`string`) +- `$format` (`int`), default `0`, optional +- `$characters` (`string`), default `null`, optional + +**Returns**: `array|int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `str_word_count` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/str_word_count.md). diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index 0b46bb728a..a6a5b8a037 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp()" description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 413 + order: 434 --- ## strcasecmp() diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index 13f648c93a..eaee8e93a1 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp()" description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 414 + order: 435 --- ## strcmp() diff --git a/docs/php/builtins/string/stripos.md b/docs/php/builtins/string/stripos.md new file mode 100644 index 0000000000..622d1e8168 --- /dev/null +++ b/docs/php/builtins/string/stripos.md @@ -0,0 +1,38 @@ +--- +title: "stripos()" +description: "Finds the numeric position of the first case-insensitive occurrence of a substring." +sidebar: + order: 436 +--- + +## stripos() + +```php +function stripos(string $haystack, string $needle, int $offset = 0): mixed +``` + +Finds the numeric position of the first case-insensitive occurrence of a substring. + +**Parameters**: +- `$haystack` (`string`) +- `$needle` (`string`) +- `$offset` (`int`), default `0`, optional + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stripos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripos.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `stripos` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/stripos.md). diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index d2e82fc5a5..cb17c76fe4 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes()" description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 415 + order: 437 --- ## stripslashes() diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index a0b2db7778..54d80f40d9 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen()" description: "Returns the length of a string." sidebar: - order: 416 + order: 438 --- ## strlen() diff --git a/docs/php/builtins/string/strncasecmp.md b/docs/php/builtins/string/strncasecmp.md new file mode 100644 index 0000000000..119704d6cd --- /dev/null +++ b/docs/php/builtins/string/strncasecmp.md @@ -0,0 +1,38 @@ +--- +title: "strncasecmp()" +description: "Compares the first n bytes of two strings, ignoring ASCII case." +sidebar: + order: 439 +--- + +## strncasecmp() + +```php +function strncasecmp(string $string1, string $string2, int $length): int +``` + +Compares the first n bytes of two strings, ignoring ASCII case. + +**Parameters**: +- `$string1` (`string`) +- `$string2` (`string`) +- `$length` (`int`) + +**Returns**: `int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `strncasecmp` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/strncasecmp.md). diff --git a/docs/php/builtins/string/strncmp.md b/docs/php/builtins/string/strncmp.md new file mode 100644 index 0000000000..00c5be8e27 --- /dev/null +++ b/docs/php/builtins/string/strncmp.md @@ -0,0 +1,38 @@ +--- +title: "strncmp()" +description: "Compares the first n bytes of two strings." +sidebar: + order: 440 +--- + +## strncmp() + +```php +function strncmp(string $string1, string $string2, int $length): int +``` + +Compares the first n bytes of two strings. + +**Parameters**: +- `$string1` (`string`) +- `$string2` (`string`) +- `$length` (`int`) + +**Returns**: `int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `strncmp` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/strncmp.md). diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index 873777fec6..8afff3a8c3 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos()" description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 417 + order: 441 --- ## strpos() diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index 3d17920826..88d99f1148 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev()" description: "Reverses a string." sidebar: - order: 418 + order: 442 --- ## strrev() diff --git a/docs/php/builtins/string/strripos.md b/docs/php/builtins/string/strripos.md new file mode 100644 index 0000000000..b9ff53772c --- /dev/null +++ b/docs/php/builtins/string/strripos.md @@ -0,0 +1,38 @@ +--- +title: "strripos()" +description: "Finds the numeric position of the last case-insensitive occurrence of a substring." +sidebar: + order: 443 +--- + +## strripos() + +```php +function strripos(string $haystack, string $needle, int $offset = 0): mixed +``` + +Finds the numeric position of the last case-insensitive occurrence of a substring. + +**Parameters**: +- `$haystack` (`string`) +- `$needle` (`string`) +- `$offset` (`int`), default `0`, optional + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strripos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strripos.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `strripos` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/strripos.md). diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index 04e8f6d4d2..3b36dd8058 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos()" description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 419 + order: 444 --- ## strrpos() diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index 4f113a36f4..bdc0386c53 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr()" description: "Returns the portion of a string starting at the first occurrence of a substring, or false." sidebar: - order: 420 + order: 445 --- ## strstr() diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index e8c39fd10b..f69bc8e1d1 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower()" description: "Converts a string to lowercase." sidebar: - order: 421 + order: 446 --- ## strtolower() diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index dc0503cbad..c6e5a1cc57 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper()" description: "Converts a string to uppercase." sidebar: - order: 422 + order: 447 --- ## strtoupper() diff --git a/docs/php/builtins/string/strtr.md b/docs/php/builtins/string/strtr.md new file mode 100644 index 0000000000..52c799a349 --- /dev/null +++ b/docs/php/builtins/string/strtr.md @@ -0,0 +1,38 @@ +--- +title: "strtr()" +description: "Translates bytes pairwise, or applies longest-match-first replacement pairs." +sidebar: + order: 448 +--- + +## strtr() + +```php +function strtr(string $string, array|string $from, string $to = null): string +``` + +Translates bytes pairwise, or applies longest-match-first replacement pairs. + +**Parameters**: +- `$string` (`string`) +- `$from` (`array|string`) +- `$to` (`string`), default `null`, optional + +**Returns**: `string` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strtr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtr.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `strtr` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/strtr.md). diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index 4910891c5a..69e9a5b27d 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr()" description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 423 + order: 449 --- ## substr() diff --git a/docs/php/builtins/string/substr_count.md b/docs/php/builtins/string/substr_count.md new file mode 100644 index 0000000000..46696b1774 --- /dev/null +++ b/docs/php/builtins/string/substr_count.md @@ -0,0 +1,39 @@ +--- +title: "substr_count()" +description: "Counts the number of non-overlapping substring occurrences." +sidebar: + order: 450 +--- + +## substr_count() + +```php +function substr_count(string $haystack, string $needle, int $offset = 0, mixed $length = null): int +``` + +Counts the number of non-overlapping substring occurrences. + +**Parameters**: +- `$haystack` (`string`) +- `$needle` (`string`) +- `$offset` (`int`), default `0`, optional +- `$length` (`mixed`), default `null`, optional + +**Returns**: `int` + +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + +## Internals + +For how `substr_count` is implemented in the compiler, see [the internals page](../../../internals/builtins/string/substr_count.md). diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index bbfd229420..d56ddf1e1f 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace()" description: "Replaces text within a portion of a string." sidebar: - order: 424 + order: 451 --- ## substr_replace() diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 4573c89df8..8b8bd041b3 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim()" description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 425 + order: 452 --- ## trim() diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index 1ad16f8be5..2181b294ec 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst()" description: "Uppercases the first character of a string." sidebar: - order: 426 + order: 453 --- ## ucfirst() diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index ceaa7a5823..3929238311 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords()" description: "Uppercases the first character of each word in a string." sidebar: - order: 427 + order: 454 --- ## ucwords() diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index e1c1201a45..c60ddaa5e6 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode()" description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 428 + order: 455 --- ## urldecode() diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index 7e4efb9e9b..39b11b9813 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode()" description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 429 + order: 456 --- ## urlencode() diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index 5d61eec478..085990f892 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf()" description: "Outputs a formatted string using an array of values." sidebar: - order: 430 + order: 457 --- ## vprintf() diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index 2dbfcf0ca9..7624485e22 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf()" description: "Returns a formatted string using an array of values." sidebar: - order: 431 + order: 458 --- ## vsprintf() diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index d7342a1ec3..0ac274878c 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap()" description: "Wraps a string to a given number of characters." sidebar: - order: 432 + order: 459 --- ## wordwrap() diff --git a/docs/php/builtins/type.md b/docs/php/builtins/type.md index a14755a382..6cbd39d379 100644 --- a/docs/php/builtins/type.md +++ b/docs/php/builtins/type.md @@ -18,7 +18,7 @@ sidebar: | [`get_resource_id()`](./type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | | [`get_resource_type()`](./type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | | [`gettype()`](./type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | -| [`intval()`](./type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`intval()`](./type/intval.md) | `(mixed $value, int $base = 10): int` | `int` | ✓ | ✓ | | [`is_array()`](./type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_bool()`](./type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | | [`is_callable()`](./type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index bcf65c8360..6b6ae9b9fa 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval()" description: "Returns the boolean value of a variable." sidebar: - order: 433 + order: 460 --- ## boolval() diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index 18f31a29d8..95448458fc 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum()" description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 434 + order: 461 --- ## ctype_alnum() diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index 4b840c335b..8479503c30 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha()" description: "Checks if all characters in the string are alphabetic." sidebar: - order: 435 + order: 462 --- ## ctype_alpha() diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index acc31a6b6e..8649d4ac98 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit()" description: "Checks if all characters in the string are digits." sidebar: - order: 436 + order: 463 --- ## ctype_digit() diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index 8813b32c23..2c47639a3a 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space()" description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 437 + order: 464 --- ## ctype_space() diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index 4811c3bdd8..6d03c06393 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval()" description: "Returns the float value of a variable." sidebar: - order: 438 + order: 465 --- ## floatval() diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index 267421b50d..46b5edc5f7 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id()" description: "Returns an integer identifier for the given resource." sidebar: - order: 439 + order: 466 --- ## get_resource_id() diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index 2b1f14b933..59b480e930 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type()" description: "Returns the type of a resource." sidebar: - order: 440 + order: 467 --- ## get_resource_type() diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index 09614d50f0..c01e8b302f 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype()" description: "Returns the type of a variable as a string." sidebar: - order: 441 + order: 468 --- ## gettype() diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index f569f0bfcb..7a48bfd5e1 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -1,20 +1,21 @@ --- title: "intval()" -description: "Returns the integer value of a variable." +description: "Returns the integer value of a variable, optionally using a given base." sidebar: - order: 442 + order: 469 --- ## intval() ```php -function intval(mixed $value): int +function intval(mixed $value, int $base = 10): int ``` -Returns the integer value of a variable. +Returns the integer value of a variable, optionally using a given base. **Parameters**: - `$value` (`mixed`) +- `$base` (`int`), default `10`, optional **Returns**: `int` diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index fe2c50236d..5a5cb38e9d 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array()" description: "Checks whether a variable is an array." sidebar: - order: 443 + order: 470 --- ## is_array() diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index ca80ebb695..d579dcbf3a 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool()" description: "Checks whether a variable is a boolean." sidebar: - order: 444 + order: 471 --- ## is_bool() diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index f407d579a1..1fba12cda2 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable()" description: "Checks whether a variable can be called as a function." sidebar: - order: 445 + order: 472 --- ## is_callable() diff --git a/docs/php/builtins/type/is_double.md b/docs/php/builtins/type/is_double.md index d692455453..efa3355869 100644 --- a/docs/php/builtins/type/is_double.md +++ b/docs/php/builtins/type/is_double.md @@ -2,7 +2,7 @@ title: "is_double()" description: "Alias of is_float()." sidebar: - order: 446 + order: 473 --- ## is_double() diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index 7d83583705..2d738ee902 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float()" description: "Checks whether a variable is a floating-point number." sidebar: - order: 447 + order: 474 --- ## is_float() diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index b3d6ffb0ad..74206f663c 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int()" description: "Checks whether a variable is an integer." sidebar: - order: 448 + order: 475 --- ## is_int() diff --git a/docs/php/builtins/type/is_integer.md b/docs/php/builtins/type/is_integer.md index 52e7159f9d..6d19ffccfc 100644 --- a/docs/php/builtins/type/is_integer.md +++ b/docs/php/builtins/type/is_integer.md @@ -2,7 +2,7 @@ title: "is_integer()" description: "Alias of is_int()." sidebar: - order: 449 + order: 476 --- ## is_integer() diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index a6abc0cc22..9d9d12268c 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable()" description: "Checks whether a variable is iterable." sidebar: - order: 450 + order: 477 --- ## is_iterable() diff --git a/docs/php/builtins/type/is_long.md b/docs/php/builtins/type/is_long.md index 3c157962c3..980d41c6ce 100644 --- a/docs/php/builtins/type/is_long.md +++ b/docs/php/builtins/type/is_long.md @@ -2,7 +2,7 @@ title: "is_long()" description: "Alias of is_int()." sidebar: - order: 451 + order: 478 --- ## is_long() diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index 7cf5eb7cea..bd19d460db 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null()" description: "Checks whether a variable is null." sidebar: - order: 452 + order: 479 --- ## is_null() diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index 1b7dc7138a..56784c0e39 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric()" description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 453 + order: 480 --- ## is_numeric() diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index 5fe31f4bf9..2a27d6225d 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object()" description: "Checks whether a variable is an object." sidebar: - order: 454 + order: 481 --- ## is_object() diff --git a/docs/php/builtins/type/is_real.md b/docs/php/builtins/type/is_real.md index 7f740ee7f9..829e09c9d6 100644 --- a/docs/php/builtins/type/is_real.md +++ b/docs/php/builtins/type/is_real.md @@ -2,7 +2,7 @@ title: "is_real()" description: "Alias of is_float()." sidebar: - order: 455 + order: 482 --- ## is_real() diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index 0e9ccec68d..e50e09eb5f 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource()" description: "Checks whether a variable is a resource." sidebar: - order: 456 + order: 483 --- ## is_resource() diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index 9c4568db64..3293f4295c 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar()" description: "Checks whether a variable is a scalar." sidebar: - order: 457 + order: 484 --- ## is_scalar() diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index 0a5ea2342a..d516ac8e3e 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string()" description: "Checks whether a variable is a string." sidebar: - order: 458 + order: 485 --- ## is_string() diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index 0731cb8a9f..460fc5ef0d 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype()" description: "Sets the type of a variable." sidebar: - order: 459 + order: 486 --- ## settype() diff --git a/docs/php/builtins/type/strval.md b/docs/php/builtins/type/strval.md index e1b84ae72d..e2e7c89aa2 100644 --- a/docs/php/builtins/type/strval.md +++ b/docs/php/builtins/type/strval.md @@ -2,7 +2,7 @@ title: "strval()" description: "Gets the string value of a variable." sidebar: - order: 460 + order: 487 --- ## strval() diff --git a/docs/php/classes.md b/docs/php/classes.md index 0b3bf83e6e..c0c98fa16f 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -874,11 +874,88 @@ echo isset($config->debug) ? "on" : "off"; // off → __isset `isset($obj->prop)` returns the boolean result of `__isset`; `unset($obj->prop)` runs `__unset` for its side effects. Both fire only for properties the class does -not declare — accessing a declared property uses it directly. +not declare *or cannot access from the calling scope* — a property the caller can +see is read, tested, and removed directly, exactly as in PHP. Contract: `__isset` and `__unset` must be non-static and public, and each takes exactly one argument (the property name). `__isset` returns `bool`. +### `unset()` on a declared property + +`unset($obj->prop)` on a property the caller can see removes it from the instance +without consulting `__unset`. For a **typed** property PHP returns the slot to its +*uninitialized* state, and elephc reproduces that exactly: + +```php +id, $row->label); + +var_dump(isset($row->id)); // false +print_r($row); // Row Object ( ) — both properties are gone +echo $row->id; // Error: Typed property Row::$id must not be + // accessed before initialization +$row->id = 9; // assigning again brings the property back +var_dump(isset($row->id)); // true +``` + +`isset()` on a typed property that was never initialized (or was unset) answers +`false` without raising the uninitialized-read error, matching PHP. + +### `unset()` on a dynamic property + +A property that lives in the object's **dynamic-property hash** — every `stdClass` +property, and any undeclared name on an `#[AllowDynamicProperties]` class — is +genuinely removable, so `unset()` matches PHP exactly: the key disappears, `isset()` +answers `false`, the value renderers stop listing it, unsetting the same name twice +or unsetting a name that was never set are both no-ops, and a later write recreates +the property at the end of the property order. + +```php +a = 1; +$o->b = "two"; +unset($o->a); + +var_dump(isset($o->a)); // false +echo json_encode($o); // {"b":"two"} +unset($o->a); // no-op +unset($o->never_set); // no-op +$o->a = 9; +echo json_encode($o); // {"b":"two","a":9} +``` + +Declared slots on an `#[AllowDynamicProperties]` class keep the fixed-slot rules +above; only undeclared names go through the hash. + +One shape is refused here: a class that declares `__unset()` **and** allows dynamic +properties. PHP calls `__unset()` only when the dynamic name is absent at the unset +site and removes the entry silently when it is present, so the choice depends on +runtime state that elephc resolves statically. It reports an unsupported-feature +diagnostic instead of guessing. + +### `unset()` limitations + +`unset()` on an **untyped** declared property (`public $foo = 1;`) is rejected at +compile time with an unsupported-feature diagnostic naming that shape. PHP truly +removes such a property: a later read emits `Warning: Undefined property: C::$foo` +and answers `null`, and a later write recreates it. elephc gives every declared +property a fixed, monomorphically typed slot — `public $foo = 1;` is stored as an +`int` — and that slot has no encoding for "removed, and reading as null": every +candidate encoding answers `int(0)` or a raw marker word instead. A loud compile +error beats a wrong value. Declare a type (`public mixed $foo = 1;` keeps the +"anything goes" storage) when the property needs to be unset, or make the property +dynamic. Note that the typed form follows PHP's *typed* rules afterwards: the slot +becomes uninitialized, so a later read raises `Error: Typed property … must not be +accessed before initialization` instead of warning and answering `null`. + +`unset()` on a **by-reference** property (`public function __construct(public int +&$p) {}`) is rejected for the same reason: the slot holds an object-owned reference +cell that the destructor still frees and that a later write would write *through*, +which would revive the very alias `unset()` is supposed to break. + ## Static call interception (`__callStatic`) `__callStatic` is the static counterpart of `__call`: a static call to a method @@ -1454,5 +1531,6 @@ Constants are inherited from parents and implemented interfaces (transitively). ## Limitations - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. +- `unset()` is supported on typed declared properties (the slot becomes uninitialized) and on dynamic properties (`stdClass`, undeclared names on `#[AllowDynamicProperties]` classes, where the entry is removed). It is rejected on untyped declared properties, on by-reference properties, and on dynamic names of a class that also declares `__unset()`. See "`unset()` limitations" above. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. - Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array/object parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/control-structures.md b/docs/php/control-structures.md index ce175e523e..436d83e537 100644 --- a/docs/php/control-structures.md +++ b/docs/php/control-structures.md @@ -7,19 +7,38 @@ sidebar: ## declare -`declare(strict_types=1);` is accepted at the top of a file. elephc compiles a -statically-typed subset and is **always strict**, so the directive is parsed and -treated as a no-op rather than toggling a runtime mode. The `ticks` and `encoding` -directives are likewise accepted and ignored. Directive values must be PHP -literals; `strict_types` must be the first statement, use the statement form, and -have the integer value `0` or `1`. +`declare(strict_types=1);` switches the file to PHP's strict parameter binding. +Like PHP, the directive is scoped to the **physical file it appears in**: it does +not propagate into files that file includes, it does not reach back into the file +that included it, and it is the file containing the *call site* — not the file +declaring the callee — that decides which rules apply. + +Under the directive, a declared scalar parameter accepts only an argument of +exactly that type, plus PHP's one surviving widening of `int` into a declared +`float`. Every other conversion PHP performs in coercive mode is a compile error +naming the `TypeError` PHP would throw — see +[Types → Strict types](./types.md#strict-types) for the full table and the +surfaces the directive does and does not reach, and +[Types → Parameter type coercion](./types.md#parameter-type-coercion) for what a +file without the directive accepts. + +`declare(strict_types=0);` is the explicit spelling of PHP's default and changes +nothing. + +The `ticks` and `encoding` directives are accepted and ignored. +Directive values must be PHP literals; `strict_types` must be the first +statement, use the statement form, and have the integer value `0` or `1`. ```php 0) { } ``` +PHP's alternative `if ($x): … elseif … else: … endif;` form is also accepted — +see [Alternative syntax](#alternative-syntax). + ## while ```php @@ -114,6 +136,34 @@ foreach ($nums as &$value) { } ``` +The value target can also be a destructuring pattern, in either spelling, with or +without a key: + +```php +$points = [[1, 2], [3, 4]]; +foreach ($points as [$x, $y]) { + echo "$x,$y\n"; +} +foreach ($points as list($x, $y)) { /* same thing */ } +foreach ($points as $i => [$x, $y]) { + echo "$i: $x,$y\n"; +} + +// Keyed, skipped, and nested patterns work exactly as they do in an assignment. +$rows = [["name" => "Ada", "role" => "admin"]]; +foreach ($rows as ["name" => $name, "role" => $role]) { + echo "$name is $role\n"; +} +foreach ([[1, 2, 3]] as [, $second]) { echo $second; } +foreach ([[1, [2, 3]]] as [$a, [$b, $c]]) { echo $a, $b, $c; } +``` + +A destructuring pattern binds one element per iteration and then unpacks it, so it +follows the rules in [Array destructuring](./arrays.md): keyed and unkeyed entries +cannot be mixed, and an empty pattern (`foreach ($x as [])`) is an error. The `&` +reference marker applies to a variable target, never to a whole pattern, so +`foreach ($x as &[$a, $b])` is rejected. + Use `foreach ($arr as $key => &$value)` when both the key and a mutable element reference are needed. The key itself cannot be bound by reference. By-reference value binding is currently supported only for array sources; @@ -226,6 +276,86 @@ switch ($x) { } ``` +## Alternative syntax + +`if`, `while`, `for`, `foreach`, and `switch` all accept PHP's alternative +syntax: a `:` opens the body instead of `{`, and a matching `endif;`, +`endwhile;`, `endfor;`, `endforeach;`, or `endswitch;` closes it. (`declare` +uses the same shape with `enddeclare;` — see above.) + +```php + 0): + echo "positive"; +elseif ($x < 0): + echo "negative"; +else: + echo "zero"; +endif; + +while ($i < 3): + $i++; +endwhile; + +for ($i = 0; $i < 3; $i++): + echo $i; +endfor; + +foreach ([1, 2, 3] as $value): + echo $value; +endforeach; + +switch ($x): + case 1: + echo "one"; + break; + default: + echo "other"; +endswitch; +``` + +The two forms are exactly equivalent — the alternative body compiles to the same +code as the braced one — and they nest freely in either direction, so an +alternative `if` can sit inside a braced `foreach` and vice versa. + +Two rules match PHP: + +- **One style per `if` chain.** Every branch of a given `if` must use the same + form. `if ($x) { ... } else: ... endif;` is rejected, as is + `if ($x): ... else { ... } endif;`. Note that this means `else if` (two words) + cannot be used in an alternative chain — write `elseif`. +- **The terminator needs its semicolon.** `endif`, `endwhile`, `endfor`, + `endforeach`, and `endswitch` are each followed by `;`. + +Since elephc has no inline-HTML mode, the alternative forms are a pure +readability choice rather than a templating feature. + +## goto + +**`goto` is not supported.** Both the statement and its target label are +rejected at compile time: + +```text +error[2:1]: `goto` is not supported: elephc compiles structured control flow +only, so a jump to an arbitrary label inside a function has no lowering. +Please restructure the jump with `break`, `continue`, a loop flag, or an early +`return` + +error[4:1]: `goto` labels are not supported: the label `end:` can only be +reached by `goto`, which elephc does not support. +``` + +elephc analyses control flow structurally — termination and reachability +analysis, flow-sensitive type narrowing, loop and branch pruning, and constant +propagation all assume the statement tree describes the CFG. An arbitrary +intra-function jump breaks that assumption, so the construct is rejected outright +rather than partially supported. Use `break` (including `break 2;`), `continue`, +a loop flag, or an early `return` instead; those cover PHP's common `goto` use +of bailing out of nested loops. + +`goto` is still a reserved word, so it cannot be used as a function name — but, +as in PHP, it remains valid as a method or constant name (`$obj->goto()`). + ## match expression PHP 8 style match. No fall-through, returns a value, uses strict comparison (`===`). diff --git a/docs/php/functions.md b/docs/php/functions.md index 4691c8dae4..062e8b8c79 100644 --- a/docs/php/functions.md +++ b/docs/php/functions.md @@ -34,6 +34,8 @@ function repeat(string $label, int $count): string { - Typed parameters can use default values - Function, method, constructor, closure, and arrow-function parameter hints are checked - Function, method, closure, and arrow-function return type hints are checked +- Arguments are bound to declared scalar parameters using PHP's default (coercive) rules where elephc can reproduce them exactly — `takesString(42)` passes `"42"`, `takesInt(5.0)` passes `5`. Conversions PHP decides at run time with a `Deprecated:` notice or a `TypeError` are compile errors instead. A file that opens with `declare(strict_types=1)` switches to PHP's strict binding, where only an exact type match and the `int`→`float` widening are accepted; see [Types → Parameter type coercion](./types.md#parameter-type-coercion) and [Types → Strict types](./types.md#strict-types) for the full tables +- A `callable` parameter accepts a compile-time-constant callable string (`"strtoupper"`, `"Formatter::wrap"`) as well as closures and first-class callables; see [Types → Callable strings](./types.md#callable-strings) - Variadic parameters may carry a type hint (`function f(int ...$xs)`), including on methods, closures, and arrow functions; every argument collected into the variadic is checked against the declared element type, just like a regular typed parameter. An untyped variadic accepts heterogeneous arguments. - Non-`void` declared return types must return a value on every reachable path; `throw`, `exit()`/`die()`, and infinite loops count as non-returning paths - Bare `return;` is valid only for `void` returns; use `return null;` for nullable return types @@ -41,6 +43,7 @@ function repeat(string $label, int $count): string { - Callable variables and `callable` parameters whose concrete target is known only through a runtime descriptor can also use named arguments, named-after-spread calls, and positional prefixes before indexed spreads; descriptor metadata applies parameter names, defaults, variadics, and by-reference flags at invocation time - Argument expressions are evaluated in PHP source order, then codegen normalizes the resulting values into ABI parameter order - Named arguments can follow spread arguments, as in `foo(...$args, suffix: "!")`; positional arguments cannot follow either named arguments or spread arguments +- Argument unpacking cannot follow a named argument: `foo(c: 9, ...$args)` is a compile-time error ("cannot use argument unpacking after named arguments"), matching PHP's fatal. The rule is syntactic, so it applies whatever the unpacked array contains — including a static string-keyed literal such as `foo(c: 9, ...["a" => 1])` — and on every call surface, including calls whose target is only known at run time. Back-to-back spreads (`foo(...$a, ...$b)`) and a string-keyed spread on its own (`foo(...["a" => 1])`) stay legal. - Associative-array unpacking maps string keys to named arguments (`foo(...["name" => "Ada"])`) and keeps numeric keys positional. Variable associative-array spreads can satisfy any parameter by string key, including parameters after explicit named arguments. Duplicate static string keys use PHP's last-wins behavior before argument planning. - A positional spread into a variadic function fills regular parameters first; only excess spread elements are collected into the variadic parameter. If a spread is too short to fill required parameters, the call fails instead of reading beyond the array payload. - User-defined variadic functions collect unknown named arguments into the variadic parameter using string keys @@ -57,6 +60,27 @@ function factorial($n) { echo factorial(10); // 3628800 ``` +Recursion depth is bounded by the real call stack, and running off the end is +reported instead of crashing. Every compiled function checks the stack pointer +against the measured stack floor on entry; when it is exhausted the program +writes + +``` +Fatal error: Maximum call stack size reached. Infinite recursion? +``` + +to stderr and exits with status 255 — the same class of controlled diagnostic +PHP 8.3+ produces for runaway recursion, and the same exit status PHP uses for +an uncaught fatal error. + +The floor comes from `getrlimit(RLIMIT_STACK)` minus a small reserve, so the +usable depth follows the process stack limit: roughly 50 000 frames of a small +function on a default 8 MiB stack. Function bodies that run on a coroutine +stack — generator bodies and `Fiber` callables — get a floor derived from that +coroutine's own 256 KiB stack instead, which is roughly 1 400 frames of the same +function. Deepen `ulimit -s` if a legitimately deep algorithm needs more room on +the main stack. + ## Default parameter values ```php @@ -488,6 +512,54 @@ collects integers into `$nums`, and every argument passed to the variadic is che against the declared element type, so passing an argument of the wrong type is rejected. An untyped variadic (`...$nums`) accepts heterogeneous arguments. +## Argument introspection + +`func_num_args()`, `func_get_args()` and `func_get_arg($position)` read the arguments the +current call actually received, including the surplus positional arguments PHP allows past +a function's declared parameter list: + +```php + $v) { // Prints: 0=a 1=b 2=c ``` -Explicit keys are passed through `=>`. Keys can be ints or string literals -and do not bump the auto counter: +Explicit keys are passed through `=>`. A generator tracks the largest +*integer* key it has yielded so far (starting below 0), and every keyless +`yield` emits the next one after it. So an explicit integer key greater than +every integer key yielded so far pushes the counter, and the following +keyless yields continue from `key + 1`: + +```php + "five"; // explicit key 5 — counter moves to 6 + yield "six"; // auto-key 6 + yield "seven"; // auto-key 7 +} +``` + +Explicit keys that are *not* integers (string, float, bool, null) are yielded +unchanged — generators do not apply array key coercion — and leave the +counter alone. An integer key at or below the largest one already used is +also yielded unchanged without rewinding the counter: ```php 42; // explicit key — counter unchanged + yield "k" => 42; // string key — counter unchanged + yield 3.5 => "half"; // float key, stays a float — counter unchanged yield "footer"; // auto-key 1 + yield 10 => "ten"; // counter moves to 11 + yield 2 => "two"; // lower than 10 — counter unchanged + yield "tail"; // auto-key 11 } ``` +The counter is a signed 64-bit value that wraps exactly like PHP's: a +`PHP_INT_MAX` key followed by a keyless `yield` produces `PHP_INT_MIN`. +Each generator instance owns its counter. + ## yield from `yield from ` expands at compile time to one yield per @@ -89,13 +114,27 @@ foreach (outer() as $v) { echo $v . " "; } // Prints: 0 1 2 3 99 ``` +`yield from` forwards the delegate's keys verbatim: it neither renumbers +them nor advances the outer generator's auto-key counter, so the outer and +inner keys can collide, exactly as in PHP: + +```php + "i1"; yield "i2"; } +function outer() { yield 3 => "o1"; yield from inner(); yield "o2"; } +foreach (outer() as $k => $v) { echo "$k=$v "; } +// Prints: 3=o1 100=i1 101=i2 4=o2 +``` + `yield from ` is driven by the `__rt_gen_delegate` runtime helper, which runs on the outer generator's coroutine stack: it advances the inner generator, re-yields each inner key/value through the outer suspend boundary, forwards sent values into the inner generator, and returns the inner generator's `getReturn()`. `yield from ` is -desugared into an iterator loop. Invalid non-generator, non-iterable -delegates are rejected at type-check time. +desugared into an iterator loop. Both delegation paths enter the suspend +primitive through `__rt_gen_suspend_delegated`, the entry point that skips +the auto-key bookkeeping. Invalid non-generator, non-iterable delegates are +rejected at type-check time. Like PHP, `yield from` also evaluates to the delegated generator's terminal `return` value, so the outer generator can capture and yield or @@ -416,14 +455,18 @@ symbols: into the body's parameter registers, runs the body, and parks the body's return value in the `return_value` slot. -Each `yield` lowers to `__rt_gen_suspend(key, value)`, which records the -boxed key/value into the generator's `last_key`/`last_value` slots and then +Each `yield` lowers to `__rt_gen_suspend(key, value)`, which applies the +auto-key bookkeeping (a NULL key consumes the counter; an explicit integer +key above the largest one used pushes it to `key + 1`), records the boxed +key/value into the generator's `last_key`/`last_value` slots, and then suspends the coroutine. Because the suspend boundary re-raises a scheduled exception **inside** the coroutine's own stack, `Generator::throw()` lands in a `try`/`catch` inside the body. `yield from ` is driven by `__rt_gen_delegate`, which forwards sent values into the inner generator and returns its `getReturn()`; `yield from ` is desugared into an -iterator loop that re-yields each entry. +iterator loop that re-yields each entry. Both enter the suspend primitive at +`__rt_gen_suspend_delegated`, which stores the forwarded key without +touching the counter. The synthetic `Generator` class has no PHP body — its method dispatch is intercepted in the codegen and routed directly to the `__rt_gen_*` diff --git a/docs/php/math.md b/docs/php/math.md index c208b9639c..eedc20bb88 100644 --- a/docs/php/math.md +++ b/docs/php/math.md @@ -9,21 +9,21 @@ sidebar: | Function | Signature | Description | |---|---|---| -| `abs()` | `abs($val): int\|float` | Absolute value (preserves type) | +| `abs()` | `abs($val): int\|float` | Absolute value (preserves type); `abs(PHP_INT_MIN)` has no `int` result and promotes to `float` | | `floor()` | `floor($val): float` | Round down | | `ceil()` | `ceil($val): float` | Round up | -| `round()` | `round($val [, $precision]): float` | Round to nearest | +| `round()` | `round($num [, $precision [, $mode]]): float` | Round to nearest. `$mode` is one of `PHP_ROUND_HALF_UP` (default), `PHP_ROUND_HALF_DOWN`, `PHP_ROUND_HALF_EVEN`, `PHP_ROUND_HALF_ODD`; any other value throws `\ValueError` | | `sqrt()` | `sqrt($val): float` | Square root | | `pow()` | `pow($base, $exp): float` | Exponentiation | -| `min()` | `min($a, $b, ...): int\|float` | Minimum (variadic) | -| `max()` | `max($a, $b, ...): int\|float` | Maximum (variadic) | +| `min()` | `min($value, ...$values): mixed` | Minimum. Either one array, or two or more values | +| `max()` | `max($value, ...$values): mixed` | Maximum. Either one array, or two or more values | | `clamp()` | `clamp(?mixed $value, ?mixed $min, ?mixed $max): ?mixed` | Clamp a value to inclusive bounds | | `intdiv()` | `intdiv($a, $b): int` | Integer division; a zero divisor raises a catchable `DivisionByZeroError`, and `intdiv(PHP_INT_MIN, -1)` an `ArithmeticError` | | `fmod()` | `fmod($a, $b): float` | Float modulo | | `fdiv()` | `fdiv($a, $b): float` | Float division (returns INF for /0) | | `rand()` | `rand([$min, $max]): int` | Random integer | -| `mt_rand()` | `mt_rand([$min, $max]): int` | Alias for rand() | -| `random_int()` | `random_int($min, $max): int` | Cryptographic random | +| `mt_rand()` | `mt_rand([$min, $max]): int` | Alias for rand(), except that `$min > $max` throws `\ValueError` instead of swapping the bounds | +| `random_int()` | `random_int($min, $max): int` | Cryptographic random. `$min > $max` throws `\ValueError`. | | `sin()` | `sin($angle): float` | Sine (radians) | | `cos()` | `cos($angle): float` | Cosine (radians) | | `tan()` | `tan($angle): float` | Tangent (radians) | @@ -42,6 +42,7 @@ sidebar: | `deg2rad()` | `deg2rad($degrees): float` | Degrees to radians | | `rad2deg()` | `rad2deg($radians): float` | Radians to degrees | | `pi()` | `pi(): float` | Returns M_PI | +| `base_convert()` | `base_convert($num, $from_base, $to_base): string` | Re-render a numeral between two bases from 2 to 36. Letter digits are case-insensitive and characters that are not digits of `$from_base` are ignored. A base outside 2-36 throws `\ValueError`. | `clamp()` validates the bounds before selecting a result. It throws `ValueError` if `$min > $max` or if either bound is `NAN`. Selection checks the upper bound first, then the lower bound. @@ -51,6 +52,82 @@ echo clamp(3.5, 0.0, 10.0); // 3.5 echo clamp("P", "A", "Z"); // "P" ``` +### base_convert() + +`base_convert()` parses `$num` in `$from_base` and re-renders it in `$to_base`. Digits above +`9` use the letters `a`-`z` in either case, and any character that is not a digit of +`$from_base` is skipped rather than ending the scan. + +```php +echo base_convert("ff", 16, 10); // 255 +echo base_convert("a37334", 16, 2); // 101000110111001100110100 +echo base_convert("zz", 36, 10); // 1295 +``` + +A value larger than `PHP_INT_MAX` widens to a float during the parse, exactly as in reference +PHP, and the rendered digits are then rounded rather than exact — `base_convert("ffffffffffffffff", 16, 10)` +is `"18446744073709552046"`, not `"18446744073709551615"`. The float render also stops after 64 +digits. + +### min() and max() + +Both accept PHP's two call forms: a single array whose elements are compared, or +two or more values compared against each other. + +```php +var_dump(min([1, 2, 3])); // int(1) +var_dump(max([1, 2, 3])); // int(3) +var_dump(min([3.5, 1.25])); // float(1.25) +var_dump(min(4, 9, 2)); // int(2) +``` + +An empty array has no element to return, so it throws a catchable `ValueError` +exactly like PHP: + +```php +try { + min([]); +} catch (ValueError $e) { + echo $e->getMessage(); // min(): Argument #1 ($value) must contain at least one element +} +``` + +The single-array form accepts indexed arrays of `int`, `float`, `bool` and `string`, +indexed arrays with heterogeneous (boxed `mixed`) elements, and associative arrays with +values of any type: + +```php +var_dump(min([1, 2.5])); // int(1) +var_dump(max(["a", "c", "b"])); // string(1) "c" +var_dump(min(["a" => 3, "b" => 1])); // int(1) +var_dump(max(["x" => "pear", "y" => "fig"])); // string(4) "pear" +``` + +Elements are compared with PHP 8's own comparison rules, in PHP's order: a `bool` on +either side converts both sides to `bool`, then `null` (which becomes `""` against a +string, so `min([null, "a"])` is `NULL` but `min(["", null])` is `""`), then two numeric +strings compare numerically while any other string pair compares byte-wise, and finally a +number against a non-numeric string compares as strings. Ties keep the *earlier* element +and the winner keeps its original type, exactly like PHP: + +```php +var_dump(min(["10", "9"])); // string(1) "9" — both numeric, so 9 < 10 +var_dump(min(["10", "9a"])); // string(2) "10" — "9a" is not numeric, so bytes decide +var_dump(max([0, "a"])); // string(1) "a" — "0" vs "a" as strings +var_dump(min([1, "1"])); // int(1) — equal, so the first element wins +``` + +Two limitations remain in the single-array form: + +- Comparisons that involve a *numeric string* are resolved as `double`s, so two integer + strings that differ only beyond 2^53 (`min(["9223372036854775807", "9223372036854775806"])`) + can compare equal where PHP compares them exactly. Comparisons between two real `int` + elements are exact. This is the same simplification the `==` runtime already makes. +- Arrays, objects, resources and callables only rank *above* the scalar elements and + compare equal to each other, instead of PHP's element-wise array comparison. An indexed + array whose elements are themselves arrays (`min([[1], [2]])`) is rejected at compile + time rather than reduced with the wrong order. + ## Math constants | Constant | Type | Value | diff --git a/docs/php/namespaces.md b/docs/php/namespaces.md index 888fcd7f4a..302c78b33d 100644 --- a/docs/php/namespaces.md +++ b/docs/php/namespaces.md @@ -35,7 +35,14 @@ Supported forms: `use Foo\Bar;`, `use Foo\Bar as Baz;`, `use function`, `use con ## Name resolution rules - Unqualified class names honor `use` aliases, otherwise resolve relative to current namespace - Functions/constants: `use function`/`use const` aliases first, then current namespace, then global fallback -- Fully-qualified `\Lib\Tool` always refers to global canonical name +- Qualified names (containing `\` but not starting with one) have their **first segment** expanded + through the class/namespace import table — the plain `use` table — whether the name denotes a + class, a function, or a constant. With `use App\Math as M;`, all of `M\double(5)`, + `new M\Thing()`, `M\Thing::method()`, `$x instanceof M\Thing` and `M\FOO` resolve into + `App\Math`. Only the first segment is substituted, and `use function` / `use const` aliases do + not participate (they apply to unqualified names only), matching PHP +- Fully-qualified `\Lib\Tool` always refers to global canonical name; a leading `\` suppresses + alias expansion, so `\M\double()` is *not* rewritten - Included files keep their own namespace and imports; an include cannot inherit the caller's namespace scope ## Case sensitivity @@ -480,6 +487,15 @@ define("PI", 3.14159); | `FNM_PATHNAME` | int | Target-specific libc/PHP value | | `FNM_PERIOD` | int | 4 | | `FNM_CASEFOLD` | int | 16 | +| `STR_PAD_RIGHT` | int | 1 — `str_pad()`'s default padding mode | +| `STR_PAD_LEFT` | int | 0 | +| `STR_PAD_BOTH` | int | 2 | +| `COUNT_NORMAL` | int | 0 — `count()`'s default mode | +| `COUNT_RECURSIVE` | int | 1 | +| `PHP_ROUND_HALF_UP` | int | 1 — `round()`'s default mode | +| `PHP_ROUND_HALF_DOWN` | int | 2 | +| `PHP_ROUND_HALF_EVEN` | int | 3 | +| `PHP_ROUND_HALF_ODD` | int | 4 | ## Superglobals diff --git a/docs/php/operators.md b/docs/php/operators.md index 40b83cda1f..a22913b771 100644 --- a/docs/php/operators.md +++ b/docs/php/operators.md @@ -12,17 +12,18 @@ sidebar: | `+` | `$a + $b` | Numeric addition, or PHP array union when both operands are arrays. Integer overflow promotes to `double`. | | `-` | `$a - $b` | Subtraction. Integer overflow promotes to `double`. | | `*` | `$a * $b` | Multiplication. Integer overflow promotes to `double`. | -| `/` | `$a / $b` | Division (always returns float) | -| `%` | `$a % $b` | Modulo | -| `**` | `$a ** $b` | Exponentiation (right-associative, returns float) | +| `/` | `$a / $b` | Division (always returns float). A zero divisor raises a catchable `DivisionByZeroError` ("Division by zero"). | +| `%` | `$a % $b` | Modulo. A zero divisor raises a catchable `DivisionByZeroError` ("Modulo by zero"); `PHP_INT_MIN % -1` is `0`. | +| `**` | `$a ** $b` | Exponentiation (right-associative). Int-preserving like PHP: two `int` operands with a non-negative exponent give an `int` while the result fits (`2 ** 3` is `int(8)`), and promote to `double` at the multiplication that overflows (`2 ** 63`). A negative exponent or a `float` operand always gives a `float`. | | `-$x` | `-$x` | Unary negation | ## Comparison | Operator | Example | Notes | |---|---|---| -| `==` | `$a == $b` | Loose equality using PHP-style scalar coercions for bool, null, numeric `int`/`float` comparison, numeric strings, and non-numeric strings | -| `!=` | `$a != $b` | Loose inequality using the same scalar coercions as `==` | +| `==` | `$a == $b` | Loose equality using PHP-style coercions for bool, null, numeric `int`/`float` comparison, numeric strings, non-numeric strings, arrays, and objects | +| `!=` | `$a != $b` | Loose inequality using the same coercions as `==` | +| `<>` | `$a <> $b` | PHP's alias for `!=`: identical semantics, identical precedence and associativity | | `===` | `$a === $b` | Strict equality (type and value) | | `!==` | `$a !== $b` | Strict inequality | | `<` | `$a < $b` | Less than | @@ -36,6 +37,60 @@ sidebar: Direct object values and boxed `mixed` / nullable / union values are checked at runtime; scalar, array, and null payloads return `false` after the dynamic target has been validated. Dynamic string targets are matched case-insensitively against class/interface names; unknown class strings return `false`. Dynamic object targets use the target object's runtime class. If a dynamic target is neither a string nor an object, the program exits with a fatal runtime diagnostic. +### Loose equality for arrays and objects + +`==` follows PHP 8's comparison table for non-scalar operands as well. + +**Arrays.** An array converts to `bool` only against `null` and `bool`; against +anything else it is simply not equal. + +```php + 1, "b" => 2] == ["b" => 2, "a" => 1]); // true +var_dump([1, 2] == [2 => 1, 3 => 2]); // false — different keys +var_dump([1] == ["1"]); // true — values compare loosely +var_dump(["a" => null] == ["b" => null]); // false — different keys +``` + +**Objects.** Two objects are loosely equal when they are the same instance, or +when they share a class and every property compares loosely. `===` keeps meaning +instance identity. Enum cases are singletons, so they compare by identity in both +forms. + +```php +x = 2; +var_dump($a == $b); // false +``` + +Known divergence: PHP raises `Fatal error: Nesting level too deep - recursive +dependency?` when `==` meets a cyclic array/object graph. elephc's comparison +walker stops at a fixed nesting depth and reports "not equal" instead of failing, +so a cyclic comparison terminates normally rather than aborting the program. + +`===` on two arrays is not yet supported by the backend and reports an +unsupported-feature diagnostic at compile time. + ## Bitwise | Operator | Example | Notes | @@ -44,8 +99,8 @@ Direct object values and boxed `mixed` / nullable / union values are checked at | `\|` | `$a \| $b` | Bitwise OR | | `^` | `$a ^ $b` | Bitwise XOR | | `~` | `~$a` | Bitwise NOT | -| `<<` | `$a << $b` | Left shift | -| `>>` | `$a >> $b` | Arithmetic right shift | +| `<<` | `$a << $b` | Left shift. A shift count of 64 or more yields `0`; a negative count raises a catchable `ArithmeticError` ("Bit shift by negative number"). | +| `>>` | `$a >> $b` | Arithmetic right shift. A shift count of 64 or more yields `0` for a non-negative value and `-1` for a negative one; a negative count raises a catchable `ArithmeticError`. | ## Logical @@ -234,6 +289,72 @@ Nullsafe access cannot be used as an assignment target or combined with first-cl | `--$i` | Pre-decrement | New value | | `$i--` | Post-decrement | Old value | +In statement position the target can be a local variable, an object property +(including `$this->prop`), an array element, or a static property, in either the +prefix or the postfix spelling: + +```php +$this->count++; +++$this->count; +$this->items[0]++; +$obj->count--; +--$obj->items[2]; +$totals["a"]++; +``` + +Statement position discards the operator's result, so `++$x;` and `$x++;` compile to +the same read-modify-write. Reading the result of an increment on a property or array +element — `echo $obj->n++;` — is not supported yet; assign through the statement form +first. + +`int`, `float`, `bool`, and `null` values all increment like PHP. Floats add or +subtract exactly `1.0` and stay floats: + +```php +$f = 1.5; +$f++; // float(2.5) +var_dump($f--); // float(2.5) — the post-form returns the old value +var_dump($f); // float(1.5) +``` + +`string` values increment with PHP's full rules, including the perl-style alphanumeric +carry: + +```php +$s = "az"; $s++; // string(2) "ba" +$s = "Zz"; $s++; // string(3) "AAa" +$s = "a9"; $s++; // string(2) "b0" +$s = "zz"; $s++; // string(3) "aaa" +$s = "a-"; $s++; // string(2) "a-" — the carry stops at the first non-alphanumeric byte +$s = "-a"; $s++; // string(2) "-b" +$s = ""; $s++; // string(1) "1" +``` + +The carry runs over raw bytes from the end of the string: `a`–`y`, `A`–`Y` and `0`–`8` +advance in place, `z`/`Z`/`9` wrap to `a`/`A`/`0` and carry into the previous byte, and +a carry out of the front prepends `a`, `A` or `1`. Any other byte (including the bytes +of a multi-byte character) stops the carry and leaves the rest of the string alone. + +A *numeric* string increments as a number instead, so the operator can change the +value's type — which is why a `string` local that is a `++`/`--` target is given boxed +`mixed` storage for its whole lifetime: + +```php +$n = "9"; $n++; // int(10) +$n = "1.5"; $n++; // float(2.5) +$n = "1e3"; $n++; // float(1001) +$n = "0x1A"; $n++; // string(4) "0x1B" — "0x1A" is not a PHP numeric string +``` + +`--` follows PHP's asymmetric rule: a numeric string decrements numerically, the empty +string becomes `int(-1)`, and any other string is left **unchanged** (`"az"--` is still +`"az"`). PHP additionally raises `E_DEPRECATED` for `++` on a non-alphanumeric string +and for `--` on a non-numeric string; elephc has no runtime deprecation channel, so it +reproduces the resulting value but not the notice. + +`++`/`--` on an array, object, buffer, or pointer local stays a compile-time error, as +in PHP where it is a `TypeError`. + ## Ternary ```php diff --git a/docs/php/strings.md b/docs/php/strings.md index 8ede988fb5..82d076b51a 100644 --- a/docs/php/strings.md +++ b/docs/php/strings.md @@ -129,6 +129,31 @@ echo "[" . $s[99] . "]"; // [] Read-only. Negative indices count from end. Out-of-bounds returns empty string. +## Incrementing a string + +`++` on a string uses PHP's perl-style alphanumeric carry, which is how the +spreadsheet-column idiom works: + +```php +`implode($array): string` | Join array into string; the one-argument form joins with an empty separator | +| `number_format()` | `number_format($n [, $dec [, $dec_point, $thou_sep]]): string` | Format number. A negative `$dec` is not an error: it rounds to that power of ten and formats with no decimals. | | `sprintf()` | `sprintf($fmt, ...): string` | Format string (%s, %d, %f, %x, %e, %g, %o, %c, %%) | | `printf()` | `printf($fmt, ...): int` | Format and print | | `vsprintf()` | `vsprintf($fmt, array $values): string` | Like `sprintf()`, with the arguments supplied as an array. Each element becomes one format argument — int/float/bool/string, including the elements of a mixed array. | @@ -174,7 +201,12 @@ Read-only. Negative indices count from end. Out-of-bounds returns empty string. | `addslashes()` | `addslashes($str): string` | Escape quotes and backslashes | | `stripslashes()` | `stripslashes($str): string` | Remove escape backslashes | | `nl2br()` | `nl2br($str): string` | Insert `
` before newlines | -| `wordwrap()` | `wordwrap($str [, $width [, $break [, $cut]]]): string` | Wrap text at word boundaries; set `$cut` to break over-long words | +| `wordwrap()` | `wordwrap($str [, $width [, $break [, $cut]]]): string` | Wrap text at word boundaries; set `$cut` to break over-long words. An empty `$break`, or a `$width` of `0` together with `$cut`, throws `\ValueError`. | +| `chunk_split()` | `chunk_split($str [, $length [, $separator]]): string` | Split into fixed-length chunks, appending `$separator` after every chunk including the last. Defaults to 76-byte chunks joined by `\r\n`. An empty subject yields a single separator; a `$length` below `1` throws `\ValueError`. | +| `quotemeta()` | `quotemeta($str): string` | Prefix each of `. \ + * ? [ ^ ] $ ( )` with a backslash | +| `strtr()` | `strtr($str, $from, $to): string`
`strtr($str, array $pairs): string` | Translate bytes pairwise, truncated to the shorter of `$from`/`$to` (a later pair for the same source byte wins), or apply replacement `$pairs` longest-match-first in a single left-to-right pass with no re-substitution. Empty keys and keys longer than the subject are ignored. `$pairs` must have string values. | +| `str_word_count()` | `str_word_count($str [, $format [, $characters]]): array\|int` | Count words (`$format` 0), return them as a list (1), or map each word to its byte offset (2). A word is letters plus interior `'` and `-`, widened by every byte of `$characters`. `$format` must be an integer literal, and a value outside `0..2` throws `\ValueError`. | +| `count_chars()` | `count_chars($str [, $mode]): array\|string` | Byte-frequency information: `$mode` 0 tallies all 256 byte values, 1 only the used ones, 2 only the unused ones, 3 renders the used byte values as a string, and 4 the unused ones. `$mode` must be an integer literal, and a value outside `0..4` throws `\ValueError`. | | `bin2hex()` | `bin2hex($str): string` | Convert binary to hex | | `hex2bin()` | `hex2bin($str): string` | Convert hex to binary | | `long2ip()` | `long2ip($ip): string` | Format a 32-bit integer as a dotted-quad IPv4 address | @@ -194,6 +226,51 @@ Read-only. Negative indices count from end. Out-of-bounds returns empty string. | `hash_final()` | `hash_final($context, $binary = false): string` | Finalize a context and return the digest (hex, or raw bytes when `$binary`). | | `hash_copy()` | `hash_copy($context): HashContext` | Clone an incremental hashing context so the original and copy can diverge. | +#### `explode()` and the `$limit` argument + +`explode()` takes PHP's optional third argument: + +```php +explode(",", "a,b,c"); // ["a", "b", "c"] +explode(",", "a,b,c", 2); // ["a", "b,c"] — the last element keeps the rest +explode(",", "a,b,c", 0); // ["a,b,c"] — 0 behaves exactly like 1 +explode(",", "a,b,c", -1); // ["a", "b"] — drops the last element +explode(",", "a,b,c", -9); // [] — drops every element +``` + +An empty `$separator` throws `\ValueError: explode(): Argument #1 ($separator) must not be empty`. + +#### `str_pad()` padding modes + +`STR_PAD_RIGHT` (`1`, the default), `STR_PAD_LEFT` (`0`), and `STR_PAD_BOTH` (`2`) +are predefined constants: + +```php +str_pad("x", 4, "-", STR_PAD_LEFT); // "---x" +str_pad("x", 5, "ab", STR_PAD_BOTH); // "abxab" +``` + +Both value checks follow PHP's order: a `$len` that cannot grow the input returns +the input untouched *before* either check runs, so `str_pad("xyz", 1, "")` is +`"xyz"` and not an error. Once padding is actually required, an empty `$pad` +throws `\ValueError: str_pad(): Argument #3 ($pad_string) must not be empty` and a +`$type` outside `0..2` throws +`\ValueError: str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH`. + +#### `number_format()` and negative `$decimals` + +A negative `$decimals` is not an error in PHP. The number is rounded to that power +of ten (half away from zero, applied to the magnitude) and then formatted with no +decimals: + +```php +number_format(1234.5678, -1); // "1,230" +number_format(1234.5678, -2); // "1,200" +number_format(-1234.5678, -1); // "-1,230" +number_format(-4.9, -1); // "0" — never "-0" +number_format(1234.5678, -9); // "0" +``` + #### The `HashContext` object `hash_init()` returns a real `HashContext` **object**, matching PHP 8 (which @@ -254,7 +331,8 @@ Known divergences: | `rawurlencode()` | `rawurlencode($str): string` | URL-encode (spaces as %20) | | `rawurldecode()` | `rawurldecode($str): string` | URL-decode (RFC 3986) | | `base64_encode()` | `base64_encode($str): string` | Base64 encode | -| `base64_decode()` | `base64_decode($str): string` | Base64 decode | +| `base64_decode()` | `base64_decode($string, $strict = false): string\|false` | Base64 decode. Whitespace inside the payload is skipped and missing padding is tolerated; the default (lax) mode also drops any other character outside the Base64 alphabet, while `$strict = true` returns `false` for such a character, for data after a padding character, for a truncated final group, and for an invalid amount of padding | +| `quoted_printable_encode()` | `quoted_printable_encode($string): string` | MIME quoted-printable encode. Control bytes, `0x7F`, high-bit bytes, `=`, and a space directly before a `CR` become `=XX`; an embedded `CRLF` is kept as a hard line break; lines are folded at 75 columns with a trailing `=` | | `gzcompress()` | `gzcompress(string $data, int $level = -1): string` | Compress a string with zlib (system `libz`); `$level` is `-1` (default) or `0`–`9` | | `gzuncompress()` | `gzuncompress(string $data): string\|false` | Decompress a `gzcompress()`-produced string; `false` on a zlib error | | `gzdeflate()` | `gzdeflate(string $data, int $level = -1): string` | Compress a string into raw DEFLATE — no zlib header or trailer; `$level` is `-1` (default) or `0`–`9` | diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e561d93c03..637e5c88b4 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -20,6 +20,7 @@ sidebar: | `putenv()` | `putenv($assignment): bool` | Set environment variable ("KEY=VALUE") | | `define()` | `define($name, $value): bool` | Define a compile-time global constant with a string-literal name | | `defined()` | `defined($name): bool` | Check whether a string-literal constant name is defined | +| `constant()` | `constant($name): mixed` | Value of a global constant named by a string literal. AOT has no runtime constant table, so a dynamic name, a `Foo::BAR` class constant, and an unknown name are compile errors | | `php_uname()` | `php_uname($mode = "a"): string` | Get system information from the target runtime | | `phpversion()` | `phpversion(?string $extension = null): string\|false` | Get the targeted PHP language version, or one extension's version (`false` if it is not loaded) | | `zend_version()` | `zend_version(): string` | Get the Zend Engine version for the compile target | @@ -346,9 +347,9 @@ wrappers are documented in [Streams](streams.md). | Function | Signature | Description | |---|---|---| -| `file_get_contents()` | `file_get_contents($filename): string\|false` | Read an entire file, or `false` if it cannot be opened. A literal `phar://` URL is decoded at compile time; non-literal `phar://` is read at runtime. Native PHAR, tar-based PHAR, and zip-based PHAR containers are readable; native gzip/bzip2 entries and ZIP deflate entries are decoded transparently. Literal and runtime-string `http://`, `https://`, `ftp://`, and `ftps://` URLs open the matching wrapper, read the whole body, and return it (`false` on a failed open). | +| `file_get_contents()` | `file_get_contents($filename, $use_include_path = false, $context = null, $offset = 0, $length = null): string\|false` | Read a file, or `false` if it cannot be opened. `$offset` starts the read at a byte position, counting from the end of the data when negative; a negative offset that reaches before the first byte emits `file_get_contents(): Failed to seek to position N in the stream` and returns `false`, while an offset past the end simply returns `""`. `$length` caps the bytes returned and is bounded by what is actually available; `null` reads to the end and a negative `$length` throws `\ValueError` before the file is opened. `$use_include_path` is accepted and behaves as `false`, because elephc resolves paths against the current directory only (the same result an include path of `"."` gives). `$context` must be `null`: elephc has no stream-context plumbing on the read path, so a non-null one is a compile error rather than a silently dropped option set. A literal `phar://` URL is decoded at compile time; non-literal `phar://` is read at runtime. Native PHAR, tar-based PHAR, and zip-based PHAR containers are readable; native gzip/bzip2 entries and ZIP deflate entries are decoded transparently. Literal and runtime-string `http://`, `https://`, `ftp://`, and `ftps://` URLs open the matching wrapper, read the whole body, and return it (`false` on a failed open). | | `file_put_contents()` | `file_put_contents($filename, $data): int` | Write file | -| `file()` | `file($filename): array` | Read into array of lines | +| `file()` | `file($filename [, $flags]): array` | Read into array of lines. `$flags` accepts `FILE_IGNORE_NEW_LINES`, `FILE_SKIP_EMPTY_LINES` and `FILE_USE_INCLUDE_PATH` (accepted, no effect). The stream-context parameter is not supported. | | `file_exists()` | `file_exists($filename): bool` | Check exists | | `is_file()` | `is_file($filename): bool` | Is regular file | | `is_dir()` | `is_dir($filename): bool` | Is directory | @@ -452,9 +453,9 @@ wrappers are documented in [Streams](streams.md). | Function | Signature | Description | |---|---|---| -| `var_dump()` | `var_dump(mixed ...$values): void` | Output the type and value of each argument in source order. Homogeneous indexed arrays of `int`, `string`, `bool`, or `float` and associative arrays (hashes) print full per-element bodies (`[N]=>\n int(V)\n`, `["key"]=>\n string(…)\n`, etc.). Nested arrays/objects inside a Mixed-element array or hash print `NULL` (recursive nesting into those layouts is still pending). | -| `print_r()` | `print_r(mixed $value, bool $return = false): string\|true` | Human-readable output. Indexed arrays, associative arrays, and arbitrarily nested arrays print PHP's recursive `Array\n(\n [key] => value\n)\n` layout (unquoted keys, 4 spaces of indentation per level, `1`/empty for bool `true`/`false`, empty for `null`). With `$return = true`, the rendering is returned instead of printed; captures are limited to 64 KiB. With `$return = false`, the function prints the rendering and returns `true`. | -| `var_export()` | `var_export($value, $return = false): ?string` | Parsable representation. Renders scalars (`'…'`-quoted strings with `\\`/`\'` escaping, `true`/`false`, `NULL`, integers, and floats — an integer-valued float gains a `.0`) and arbitrarily nested arrays in PHP's `array (\n key => value,\n)` layout (2 spaces of indentation per level, integer keys bare and string keys quoted, nested arrays on their own line). With `$return = true` the rendering is returned instead of printed. Objects are not yet rendered (PHP emits `\Class::__set_state(...)`). Floats use PHP's `serialize_precision = -1` semantics — the shortest decimal that round-trips back to the same `double` (so `1/3` renders as `0.3333333333333333`, not the 14-digit `(string)` form), with the same scientific layout as PHP (`1.0E+17`, `1.0E-6`), an integer-valued float gaining `.0` (`1.0`, `100.0`), and `-0.0`, `INF`, `-INF`, `NAN` preserved. This is independent of the default `precision` used by `echo`/`(string)`. | +| `var_dump()` | `var_dump(mixed ...$values): void` | Output the type and value of each argument in source order. Homogeneous indexed arrays of `int`, `string`, `bool`, or `float` and associative arrays (hashes) print full per-element bodies (`[N]=>\n int(V)\n`, `["key"]=>\n string(…)\n`, etc.). Objects print `object(C)#N (n) { … }` with PHP's visibility-annotated keys. An enum case prints `enum(Enum::Case)` at any depth, for pure and backed enums alike. Nested arrays/objects inside a Mixed-element array or hash print `NULL` (recursive nesting into those layouts is still pending). | +| `print_r()` | `print_r(mixed $value, bool $return = false): string\|true` | Human-readable output. Indexed arrays, associative arrays, and arbitrarily nested arrays print PHP's recursive `Array\n(\n [key] => value\n)\n` layout (unquoted keys, 4 spaces of indentation per level, `1`/empty for bool `true`/`false`, empty for `null`). Objects print `ClassName Object\n(\n [prop] => value\n)\n` with PHP's `[prop:protected]` / `[prop:Class:private]` key annotations, arbitrary array/object nesting, and PHP's ` *RECURSION*` marker for a revisited instance; an enum case prints `Enum Enum`, `Enum Enum:int` or `Enum Enum:string` with its `name` (and `value`). With `$return = true`, the rendering is returned instead of printed; captures are limited to 64 KiB. With `$return = false`, the function prints the rendering and returns `true`. | +| `var_export()` | `var_export($value, $return = false): ?string` | Parsable representation. Renders scalars (`'…'`-quoted strings with `\\`/`\'` escaping, `true`/`false`, `NULL`, integers, and floats — an integer-valued float gains a `.0`) and arbitrarily nested arrays in PHP's `array (\n key => value,\n)` layout (2 spaces of indentation per level, integer keys bare and string keys quoted, nested arrays on their own line). Objects render as PHP does: `stdClass` as `(object) array(\n 'p' => …,\n)`, any other class as `\Class::__set_state(array(\n 'p' => …,\n))`, and an enum case as `\Enum::Case`. Object property names are printed bare, without a visibility suffix, matching PHP. With `$return = true` the rendering is returned instead of printed. Floats use PHP's `serialize_precision = -1` semantics — the shortest decimal that round-trips back to the same `double` (so `1/3` renders as `0.3333333333333333`, not the 14-digit `(string)` form), with the same scientific layout as PHP (`1.0E+17`, `1.0E-6`), an integer-valued float gaining `.0` (`1.0`, `100.0`), and `-0.0`, `INF`, `-INF`, `NAN` preserved. This is independent of the default `precision` used by `echo`/`(string)`. | ```php `, `>=`, `<=>`) between two **runtime** string operands is rejected at compile time with "Comparison operators require numeric operands" / "Spaceship operator requires numeric operands", where PHP compares them (numerically when both look numeric, byte-wise otherwise). The constant-folded form is not affected: `"a" <=> "b"`, `"B" < "a"` and `"10" > "9"` are evaluated at compile time with PHP's exact rules and produce PHP's answer, so only comparisons whose operands are not compile-time constants hit the restriction. - `??=` is checked against typed assignment storage for variables, object properties, static properties, and non-append array elements. For concrete local variable types, the fallback must keep the same type or be a literal `null`. - Plain array numeric casts (`(int)$array`, `(float)$array`) follow elephc's existing array cast semantics (return the element count rather than PHP's `0`/`1`). Direct `iterable` numeric casts use PHP's empty/non-empty `0`/`1` semantics. - `__destruct` runs when an object's refcount reaches zero (scope exit, reassignment, `unset`, program end), matching PHP's timing, but **object resurrection is not supported**: re-storing `$this` so the object would outlive the destructor does not keep it alive — the object is still freed once `__destruct` returns. @@ -251,7 +363,13 @@ Narrowing applies to function and method parameters. A parameter whose call site and synchronizing the affected native locals. Use an array keyed by the dynamic name in portable elephc code for now. - Reference aliases to array elements (`$b =& $a[0]`) are limited to **indexed arrays with integer indices**; associative arrays (`$b =& $a['key']`) are rejected at compile time. Referencing an out-of-range index binds the alias to a null cell instead of creating the element (PHP autovivifies it as `null`), and the alias points into the array's storage, so it is only valid while the array is alive and not reallocated by growth. +- Reference *elements* inside array literals (`$r = [&$a, &$b];`, `['k' => &$a]`, `array(&$a)`) are rejected at compile time with `Reference elements in array literals ([&$x]) are not supported`. PHP stores such an element as a reference cell aliasing the source variable, so `$r[0] = 9` writes through to `$a`. elephc's arrays hold plain values and its only reference form points *into* array storage (the `$b =& $a[0]` case above), never out of it — an element aliasing a local would be a pointer to a stack slot the array can outlive. Assign the value and copy back, or alias an existing element with `$b =& $a[0]`. +- `goto` and its target labels are not supported and are rejected at compile time (`` `goto` is not supported ``). elephc's termination analysis, flow-sensitive narrowing, loop/branch pruning, and constant propagation all read control flow from the statement tree, which an arbitrary intra-function jump invalidates. Use `break` (including `break 2;`), `continue`, a loop flag, or an early `return`. See [Control Structures](./control-structures.md#goto). +- `++` / `--` on a `string` follows PHP exactly, including the perl-style alphanumeric carry (`"az"++` is `"ba"`, `"Zz"++` is `"AAa"`) and the numeric-string retype (`"9"++` is `int(10)`, `"3.5"++` is `float(4.5)`). Because the operator can change the value's type, a `string` local that is a `++`/`--` target is given boxed `mixed` frame storage for its whole lifetime, so its runtime type follows the value rather than the declaration. The one divergence: PHP raises `E_DEPRECATED` for `++` on a non-alphanumeric string and for `--` on a non-numeric string, and elephc has no runtime deprecation channel, so it produces the same value without the notice. `++` / `--` on an array, object, buffer, or pointer local is still rejected at compile time. - `print_r($value, true)` captures into a fixed 64 KiB buffer: rendered output longer than 65536 bytes is truncated at the cap (PHP returns the full string). Echo mode (`print_r($value)`) is unaffected. +- `var_dump()`, `print_r()` and `var_export()` render an object's **declared** properties only. Dynamic (undeclared) properties — every property of a `stdClass` built with `$o->p = 1`, and any property added to an `#[\AllowDynamicProperties]` class — are not listed, so `print_r(new stdClass)` prints an empty body where PHP lists the assigned properties. All three renderers share one per-class descriptor, so they never disagree about which properties an object has. +- `func_num_args()`, `func_get_args()` and `func_get_arg()` are compiled away rather than dispatched as builtin calls, so `function_exists()` reports `false` for the three names where PHP reports `true`. Their supported scopes are also narrower than PHP's: they are rejected in a function with an optional (defaulted) parameter, in a function that already declares its own variadic, and in a method that overrides a parent method or implements an interface method. Everywhere else — functions, methods, static methods, closures, arrow functions, generators — they match PHP, including reporting the current values of the declared parameters. See [Functions](./functions.md#argument-introspection). +- Surplus *positional* arguments (PHP allows any user function to be called with more arguments than it declares, discarding the extras) are only accepted by functions that use one of the three argument-introspection constructs above. Every other user function keeps elephc's compile-time arity check, so `function f($a) {} f(1, 2);` is a compile error where PHP runs it. - `serialize()`/`unserialize()` cover scalars, arrays, and objects (including the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods and `r:`/`R:` object back-references) byte-for-byte compatibly with PHP. Remaining gaps: a cyclic reference inside an object's own properties resolves to `null` on `unserialize()` (serialization handles cycles), the deprecated `Serializable` interface (`C:` wire form) is unsupported, writing a property of an unserialized object held in a `Mixed` does not persist (a separate `Mixed` property-write limitation), and `unserialize()` does not emit PHP's `E_WARNING` / `E_NOTICE` on malformed input — it just returns `false`. ### Filesystem functions not implemented diff --git a/examples/advanced-functions/main.php b/examples/advanced-functions/main.php index c97850665c..e1bebefafa 100644 --- a/examples/advanced-functions/main.php +++ b/examples/advanced-functions/main.php @@ -76,3 +76,29 @@ function log_message(&$count, $msg) { log_message($log_count, "Processing"); log_message($log_count, "Done"); echo "Logged " . $log_count . " messages\n"; + +// --- Argument introspection --- +// PHP lets a function be called with more positional arguments than it declares. +// The surplus is reachable only through func_num_args(), func_get_args() and +// func_get_arg(). + +function describe_call() { + $parts = []; + foreach (func_get_args() as $arg) { + $parts[] = var_export($arg, true); + } + return func_num_args() . " arg(s): " . implode(", ", $parts); +} + +echo describe_call() . "\n"; +echo describe_call(1, "two", 3.0) . "\n"; + +// Declared parameters are part of the argument list too, and func_get_arg() +// reads any position by index. +function tag($label) { + $extra = func_num_args() > 1 ? func_get_arg(1) : "(none)"; + return $label . " -> " . $extra; +} + +echo tag("first") . "\n"; +echo tag("first", "second") . "\n"; diff --git a/examples/array-internal-pointer/.gitignore b/examples/array-internal-pointer/.gitignore new file mode 100644 index 0000000000..fc9576aeb9 --- /dev/null +++ b/examples/array-internal-pointer/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main diff --git a/examples/array-internal-pointer/main.php b/examples/array-internal-pointer/main.php new file mode 100644 index 0000000000..810d1a51e8 --- /dev/null +++ b/examples/array-internal-pointer/main.php @@ -0,0 +1,55 @@ + 18.5, + "tuesday" => 21.0, + "wednesday" => 19.75, + "thursday" => 23.5, + "friday" => 22.25, +]; + +// The classic cursor walk. An array starts with its pointer on the first +// element, so the reset() here is documentation rather than necessity. +echo "Week in order:\n"; +reset($readings); +while (($celsius = current($readings)) !== false) { + printf(" %-10s %5.2f C\n", key($readings), $celsius); + next($readings); +} + +// end() parks on the last element; prev() walks backwards from there. +echo "\nLast three, backwards:\n"; +end($readings); +for ($seen = 0; $seen < 3; $seen++) { + printf(" %-10s %5.2f C\n", key($readings), current($readings)); + prev($readings); +} + +// Walking off the front leaves the cursor invalid: current() is false and +// key() is null. next() does not recover it -- only reset()/end() do. +echo "\nOff the front:\n"; +reset($readings); +prev($readings); +var_dump(current($readings)); +var_dump(key($readings)); +var_dump(next($readings)); + +echo "\nBack on the rails: "; +var_dump(reset($readings)); + +// foreach never disturbs the pointer, because PHP iterates an internal copy. +next($readings); +$before = key($readings); +$total = 0.0; +foreach ($readings as $celsius) { + $total += $celsius; +} +printf("\nMean %.2f C, and the cursor is still on %s (was %s).\n", + $total / count($readings), key($readings), $before); diff --git a/examples/arrays/main.php b/examples/arrays/main.php index 0c53fb512f..f653ef99c6 100644 --- a/examples/arrays/main.php +++ b/examples/arrays/main.php @@ -67,6 +67,36 @@ function sum($arr) { } echo "\n"; +// array_splice() removes a window IN PLACE and returns what it removed; the optional +// fourth argument is spliced in where the removed window was, so the array can grow. +$queue = [10, 20, 30, 40, 50]; +$removed = array_splice($queue, 1, 2, [21, 22, 23]); +echo "Spliced queue: "; +foreach ($queue as $value) { + echo $value . " "; +} +echo "(removed " . count($removed) . ")\n"; + +// The same works on a string array, whose elements are wider than a scalar slot, and the +// replacement may change the element type: PHP just makes the array heterogeneous. +$words = ["alpha", "beta", "gamma", "delta"]; +$cut = array_splice($words, 1, 2, ["BETA"]); +echo "Spliced words: " . implode(", ", $words) . " (removed " . implode(", ", $cut) . ")\n"; + +$mixed = [1, 2, 3]; +array_splice($mixed, 1, 1, ["two", 2.5]); +echo "Promoted: " . implode(", ", $mixed) . "\n"; + +// A by-reference parameter is the caller's storage, so a builtin that relocates the array +// while prepending still reaches the original variable. +function prepend_all(array &$target): void +{ + array_unshift($target, 9, 8, 7, 6, 5, 4, 3, 2, 1); +} +$counts = [100, 200]; +prepend_all($counts); +echo "Unshifted: " . implode(",", $counts) . "\n"; + // unset() removes a key without renumbering: the array keeps its other keys (a hole) unset($squares[1]); echo "After unset(squares[1]): "; diff --git a/examples/assoc-arrays/main.php b/examples/assoc-arrays/main.php index d0a98313e7..5ec502804e 100644 --- a/examples/assoc-arrays/main.php +++ b/examples/assoc-arrays/main.php @@ -111,3 +111,18 @@ echo "\n"; } echo "As JSON: " . json_encode($profile) . "\n"; + +// Sorting an associative array reorders iteration only: keys stay on their values, +// and PHP copy-on-write keeps a copy taken before the sort in the original order. +$scores = ["bruno" => 7, "ada" => 9, "carl" => 7, "dina" => 4]; +$asEntered = $scores; +ksort($scores); +echo "\nBy key: " . implode(", ", array_keys($scores)) . "\n"; +krsort($scores); +echo "By key desc: " . implode(", ", array_keys($scores)) . "\n"; +asort($scores); +echo "By score: "; +foreach ($scores as $name => $points) { + echo $name . "=" . $points . " "; +} +echo "\nAs entered: " . implode(", ", array_keys($asEntered)) . "\n"; diff --git a/examples/callbacks/main.php b/examples/callbacks/main.php index bb1a8b4fbc..050fb2f14c 100644 --- a/examples/callbacks/main.php +++ b/examples/callbacks/main.php @@ -33,6 +33,14 @@ function show($x) { echo " " . $x . "\n"; } foreach ($sorted as $v) { echo $v . " "; } echo "\n"; +// usort over a string array: the comparator receives each element as a string +$words = ["banana", "apple", "fig", "cherry"]; +usort($words, fn($a, $b) => strlen($a) <=> strlen($b)); +echo "Sorted words: " . implode(", ", $words) . "\n"; + +// array_reduce over a string array: the callback folds strings into an int accumulator +echo "Total word length: " . array_reduce($words, fn($carry, $word) => $carry + strlen($word), 0) . "\n"; + // array_walk: apply side-effect to each element echo "Walk:\n"; $items = [10, 20, 30]; @@ -421,3 +429,26 @@ public static function run() { echo "\n"; $mixed_types = array_map(fn($value) => gettype($value), $mixed_values); echo "mixed array_map gettype: " . implode(", ", $mixed_types) . "\n"; + +// A `callable` parameter also accepts a PHP callable string when the name is known at +// compile time: a builtin, a user function, or "Class::method". +class StaticWrapper { + public static function wrap(string $value): string { + return "<" . $value . ">"; + } +} + +function apply_callable(callable $fn, string $value): string { + return $fn($value); +} + +echo "callable string builtin: " . apply_callable("strtoupper", "abc") . "\n"; +echo "callable string user function: " . apply_callable("callback_name_passthrough", "kept") . "\n"; +echo "callable string static method: " . apply_callable("StaticWrapper::wrap", "wrapped") . "\n"; + +// Scalar arguments bind to declared scalar parameters using PHP's coercive rules. +function describe_length(string $label, int $count): string { + return $label . "=" . $count; +} + +echo "coerced arguments: " . describe_length(7, "3") . "\n"; diff --git a/examples/control-flow/main.php b/examples/control-flow/main.php index 67473f2570..60123048f1 100644 --- a/examples/control-flow/main.php +++ b/examples/control-flow/main.php @@ -58,3 +58,33 @@ function classify($n) { } echo "\n"; } + +// Alternative syntax: `:` opens the body, `endif;`/`endforeach;`/`endswitch;` closes it. +// Identical semantics to the braced forms above, and the two nest in either direction. +echo "\nStock report:\n"; +$stock = ["widget" => 12, "gizmo" => 0, "doohickey" => 3]; +foreach ($stock as $item => $qty): + echo " " . str_pad($item, 12); + if ($qty === 0): + echo "out of stock\n"; + elseif ($qty < 5): + echo "low (" . $qty . ")\n"; + else: + echo "in stock (" . $qty . ")\n"; + endif; +endforeach; + +$reorder = 0; +foreach ($stock as $qty): + switch (true): + case $qty === 0: + $reorder += 20; + break; + case $qty < 5: + $reorder += 10; + break; + default: + // nothing to reorder + endswitch; +endforeach; +echo "Units to reorder: " . $reorder . "\n"; diff --git a/examples/declare-directives/main.php b/examples/declare-directives/main.php index 8e8baea3a8..18384aefa1 100644 --- a/examples/declare-directives/main.php +++ b/examples/declare-directives/main.php @@ -2,7 +2,8 @@ declare(strict_types=1); -echo "elephc always uses strict typing\n"; +// elephc has one parameter-binding model, so the directive changes nothing. +echo "strict_types is parsed and ignored\n"; declare(ticks=1) { echo "braced declare body\n"; diff --git a/examples/file-io/main.php b/examples/file-io/main.php index 450d699d9b..6ea05f9c04 100644 --- a/examples/file-io/main.php +++ b/examples/file-io/main.php @@ -8,6 +8,11 @@ $content = file_get_contents("greeting.txt"); print $content; +// Read only part of a file: $offset starts the read, $length caps it, +// and a negative $offset counts back from the end of the file. +echo "Bytes 6-9: " . file_get_contents("greeting.txt", false, null, 6, 4) . "\n"; +echo "Last line: " . file_get_contents("greeting.txt", false, null, -7); + // Check file properties if (file_exists("greeting.txt")) { echo "File exists, size: " . filesize("greeting.txt") . " bytes\n"; diff --git a/examples/math/main.php b/examples/math/main.php index 07d3d79d75..19cb22c5c5 100644 --- a/examples/math/main.php +++ b/examples/math/main.php @@ -19,6 +19,17 @@ $dist = hypot(3.0, 4.0); echo "distance(0,0 → 3,4) = " . $dist . "\n"; +echo "\n=== Base conversion ===\n"; +echo "base_convert('ff', 16, 10) = " . base_convert("ff", 16, 10) . "\n"; +echo "base_convert('255', 10, 2) = " . base_convert("255", 10, 2) . "\n"; +echo "base_convert('zz', 36, 10) = " . base_convert("zz", 36, 10) . "\n"; +// A base outside 2-36 is a catchable \ValueError +try { + base_convert("ff", 16, 64); +} catch (\ValueError $e) { + echo "caught: " . $e->getMessage() . "\n"; +} + echo "\n=== Randomness ===\n"; echo "rand(1, 10) = " . rand(1, 10) . "\n"; echo "mt_rand(100, 105) = " . mt_rand(100, 105) . "\n"; diff --git a/examples/string-ops/main.php b/examples/string-ops/main.php index ce5abc58f3..bee6b753b7 100644 --- a/examples/string-ops/main.php +++ b/examples/string-ops/main.php @@ -6,6 +6,11 @@ // Searching echo "--- Search ---\n"; echo "strpos: " . strpos($str, "World") . "\n"; +// stripos()/strripos() are the case-insensitive twins of strpos()/strrpos(); +// the optional $offset works the same way, negative values included +echo "stripos: " . stripos($str, "WORLD") . "\n"; +echo "strripos: " . strripos($str, "O") . "\n"; +echo "stripos(offset): " . stripos($str, "L", 4) . "\n"; echo "str_contains: " . (str_contains($str, "World") ? "yes" : "no") . "\n"; echo "str_starts_with: " . (str_starts_with($str, "Hello") ? "yes" : "no") . "\n"; echo "str_ends_with: " . (str_ends_with($str, "!") ? "yes" : "no") . "\n"; @@ -46,6 +51,51 @@ echo "wordwrap(15):\n" . wordwrap("The quick brown fox jumped", 15) . "\n"; echo "wordwrap(8, cut):\n" . wordwrap("A verylongword", 8, "\n", true) . "\n"; +// Chunking and escaping +echo "\n--- Chunk/Escape ---\n"; +// chunk_split() appends the separator after every chunk, including the trailing partial one +echo "chunk_split(3): " . chunk_split("abcdefgh", 3, "-") . "\n"; +// quotemeta() backslash-escapes the regular-expression metacharacters +echo "quotemeta: " . quotemeta('cost: $5 (approx.) [net]') . "\n"; +// A $length below 1 is a catchable \ValueError +try { + chunk_split("abc", 0); +} catch (\ValueError $e) { + echo "caught: " . $e->getMessage() . "\n"; +} + +// Word and byte statistics +echo "\n--- Stats ---\n"; +$sentence = "Hello friend, you're looking good today!"; +// Format 0 counts words, 1 returns the list, 2 keys every word by its byte offset +echo "str_word_count: " . str_word_count($sentence) . "\n"; +echo "str_word_count(1): " . implode(", ", str_word_count($sentence, 1)) . "\n"; +foreach (str_word_count("one two", 2) as $offset => $word) { + echo " offset $offset => $word\n"; +} +// $characters widens the word alphabet beyond letters, ' and - +echo "str_word_count digits: " . implode(", ", str_word_count("fri3nd", 1, "3")) . "\n"; +// count_chars() mode 1 tallies only the byte values the string actually uses +foreach (count_chars("hello", 1) as $byte => $count) { + echo " " . chr($byte) . " x $count\n"; +} +// modes 3 and 4 render the used / unused byte values as a string +echo "count_chars(3): " . count_chars("hello world", 3) . "\n"; +// A mode outside 0..4 is a catchable \ValueError +try { + count_chars("abc", 9); +} catch (\ValueError $e) { + echo "caught: " . $e->getMessage() . "\n"; +} + +// Translation +echo "\n--- Translate ---\n"; +// Three arguments translate bytes pairwise, truncated to the shorter list +echo "strtr(pairwise): " . strtr("abcd", "abc", "xy") . "\n"; +// Two arguments apply replacement pairs longest-match-first, in one left-to-right pass +echo "strtr(pairs): " . strtr("foo bar", ["foo" => "bar", "bar" => "baz"]) . "\n"; +echo "strtr(longest): " . strtr("abc", ["a" => "b", "ab" => "X"]) . "\n"; + // Split and join echo "\n--- Split/Join ---\n"; $csv = "one,two,three"; @@ -109,6 +159,12 @@ echo "htmlspecialchars: " . htmlspecialchars("bold") . "\n"; echo "urlencode: " . urlencode("hello world") . "\n"; echo "base64: " . base64_encode("Hello") . "\n"; +// base64_decode() skips whitespace and tolerates missing padding; $strict = true +// instead returns false for anything outside the Base64 alphabet +echo "base64_decode: " . base64_decode("SGVs bG8") . "\n"; +var_dump(base64_decode("SGVsbG8*", true)); +// quoted_printable_encode() escapes control, high-bit, and "=" bytes as =XX +echo "quoted_printable_encode: " . quoted_printable_encode("caf\xC3\xA9 = 1\tunit") . "\n"; // Validation echo "\n--- Validation ---\n"; @@ -120,3 +176,20 @@ $parsed = sscanf("X=42 Y=99", "X=%d Y=%d"); echo "sscanf count: " . count($parsed) . "\n"; echo "sscanf values: " . $parsed[0] . ", " . $parsed[1] . "\n"; + +// Increment (PHP's perl-style alphanumeric carry) +echo "\n--- Increment ---\n"; +$col = "A"; +$cols = []; +for ($i = 0; $i < 28; $i++) { $cols[] = $col; $col++; } +echo "columns: " . implode(" ", $cols) . "\n"; +$word = "az"; +$word++; +echo "'az'++ : " . $word . "\n"; +$wrap = "Zz"; +$wrap++; +echo "'Zz'++ : " . $wrap . "\n"; +$num = "9"; +$num++; +echo "'9'++ : "; +var_dump($num); diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 6314ce5d41..7d48584106 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -16,7 +16,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -94,7 +94,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -171,7 +171,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Internal helper used by the gmmktime() builtin.", "Bypasses timezone handling and calls the runtime gmmktime helper directly.", @@ -306,7 +306,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_hash_ctx_copy` through `BuiltinLoweringContext`.", @@ -409,7 +409,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_hash_ctx_final` through `BuiltinLoweringContext`.", @@ -519,7 +519,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_hash_ctx_init` through `BuiltinLoweringContext`.", @@ -622,7 +622,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_hash_ctx_update` through `BuiltinLoweringContext`.", @@ -732,7 +732,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -855,7 +855,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -964,7 +964,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Internal helper used by the mktime() builtin.", "Bypasses timezone handling and calls the runtime mktime helper directly.", @@ -1099,7 +1099,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -1180,7 +1180,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -1245,6 +1245,353 @@ "slug": "__elephc_normalize_callable", "sub_area": "Pointer" }, + { + "area": "Misc", + "canonical_name": "__elephc_object_is_enum", + "description": "Internal: reports whether a value is a PHP enum case.", + "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/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.__elephc_object_is_enum` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/__elephc_object_is_enum.rs", + "sig_line": null + }, + "name": "__elephc_object_is_enum", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_heap" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "__elephc_object_is_enum" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "__elephc_object_is_enum" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "__elephc_object_is_enum", + "sub_area": "Callables" + }, + { + "area": "Misc", + "canonical_name": "__elephc_object_prop_count", + "description": "Internal: number of renderable properties on an object.", + "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/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.__elephc_object_prop_count` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/__elephc_object_prop_count.rs", + "sig_line": null + }, + "name": "__elephc_object_prop_count", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_heap" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "__elephc_object_prop_count" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "__elephc_object_prop_count" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "__elephc_object_prop_count", + "sub_area": "Callables" + }, + { + "area": "Misc", + "canonical_name": "__elephc_object_prop_name", + "description": "Internal: bare name of an object's Nth renderable property.", + "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/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.__elephc_object_prop_name` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/__elephc_object_prop_name.rs", + "sig_line": null + }, + "name": "__elephc_object_prop_name", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_heap" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "__elephc_object_prop_name" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "__elephc_object_prop_name" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "index", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "__elephc_object_prop_name", + "sub_area": "Callables" + }, + { + "area": "Misc", + "canonical_name": "__elephc_object_prop_value", + "description": "Internal: value of an object's Nth renderable property.", + "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/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.__elephc_object_prop_value` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/__elephc_object_prop_value.rs", + "sig_line": null + }, + "name": "__elephc_object_prop_value", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_heap", + "alloc_heap" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "__elephc_object_prop_value" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "__elephc_object_prop_value" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "index", + "optional": false, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "__elephc_object_prop_value", + "sub_area": "Callables" + }, { "area": "Pointer", "canonical_name": "__elephc_pdo_adapter_addr", @@ -1262,7 +1609,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -1340,7 +1687,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -1417,7 +1764,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -1494,7 +1841,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_bzip2_archive` through `BuiltinLoweringContext`.", @@ -1597,7 +1944,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_decompress_archive` through `BuiltinLoweringContext`.", @@ -1700,7 +2047,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_file_metadata` through `BuiltinLoweringContext`.", @@ -1803,7 +2150,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_metadata` through `BuiltinLoweringContext`.", @@ -1906,7 +2253,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_signature_hash` through `BuiltinLoweringContext`.", @@ -2009,7 +2356,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_signature_type` through `BuiltinLoweringContext`.", @@ -2112,7 +2459,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_get_stub` through `BuiltinLoweringContext`.", @@ -2215,7 +2562,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_gzip_archive` through `BuiltinLoweringContext`.", @@ -2318,7 +2665,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Internal helper used by the built-in Phar / PharData support to enumerate archive entries.", "Calls the native PHAR listing bridge and returns the entries as an array.", @@ -2424,7 +2771,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Internal helper used by the built-in Phar / PharData support to change archive compression.", "Calls the native PHAR compression-control bridge and returns whether the update succeeded.", @@ -2536,7 +2883,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_file_metadata` through `BuiltinLoweringContext`.", @@ -2646,7 +2993,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_metadata` through `BuiltinLoweringContext`.", @@ -2756,7 +3103,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_stub` through `BuiltinLoweringContext`.", @@ -2866,7 +3213,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_set_zip_password` through `BuiltinLoweringContext`.", @@ -2969,7 +3316,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_sign_hash` through `BuiltinLoweringContext`.", @@ -3079,7 +3426,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_phar_sign_openssl` through `BuiltinLoweringContext`.", @@ -3189,7 +3536,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_is_null` through `BuiltinLoweringContext`.", @@ -3288,7 +3635,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_read_string` through `BuiltinLoweringContext`.", @@ -3394,7 +3741,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.__elephc_ptr_write_string` through `BuiltinLoweringContext`.", @@ -3500,7 +3847,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Internal helper used by the strtotime() builtin.", "Provides a raw timestamp parsing path for the runtime strtotime helper.", @@ -3623,7 +3970,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.abs` through `BuiltinLoweringContext`.", @@ -3721,7 +4068,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.acos` through `BuiltinLoweringContext`.", @@ -3818,7 +4165,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.add_slashes` through `BuiltinLoweringContext`.", @@ -3896,7 +4243,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_all` through `BuiltinLoweringContext`.", @@ -4002,7 +4349,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_any` through `BuiltinLoweringContext`.", @@ -4115,6 +4462,12 @@ "default": null, "name": "length", "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true } ], "required_param_count": 2, @@ -4130,7 +4483,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_chunk` through `BuiltinLoweringContext`.", @@ -4150,7 +4503,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -4194,6 +4549,13 @@ "name": "length", "optional": false, "type": "int" + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true, + "type": "bool" } ], "return_type": "array", @@ -4241,7 +4603,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_column` through `BuiltinLoweringContext`.", @@ -4352,7 +4714,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_combine` through `BuiltinLoweringContext`.", @@ -4424,6 +4786,121 @@ "slug": "array_combine", "sub_area": "Array" }, + { + "area": "Array", + "canonical_name": "array_count_values", + "description": "Counts the occurrences of each distinct value in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_count_values.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.array_count_values` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/array/array_count_values.rs", + "sig_line": null + }, + "name": "array_count_values", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "array_count_values" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "array_count_values" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "array", + "variadic": null + }, + "slug": "array_count_values", + "sub_area": "Array" + }, { "area": "Array", "canonical_name": "array_diff", @@ -4457,7 +4934,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff` through `BuiltinLoweringContext`.", @@ -4539,7 +5016,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff_assoc` through `BuiltinLoweringContext`.", @@ -4637,7 +5114,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_diff_key` through `BuiltinLoweringContext`.", @@ -4747,7 +5224,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_fill` through `BuiltinLoweringContext`.", @@ -4767,7 +5244,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -4865,7 +5344,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_fill_keys` through `BuiltinLoweringContext`.", @@ -4982,7 +5461,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_filter` through `BuiltinLoweringContext`.", @@ -5095,7 +5574,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_find` through `BuiltinLoweringContext`.", @@ -5217,7 +5696,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_flip` through `BuiltinLoweringContext`.", @@ -5315,7 +5794,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect` through `BuiltinLoweringContext`.", @@ -5397,7 +5876,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect_assoc` through `BuiltinLoweringContext`.", @@ -5495,7 +5974,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_intersect_key` through `BuiltinLoweringContext`.", @@ -5577,7 +6056,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_is_list` through `BuiltinLoweringContext`.", @@ -5681,7 +6160,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_exists` through `BuiltinLoweringContext`.", @@ -5770,7 +6249,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_first` through `BuiltinLoweringContext`.", @@ -5852,7 +6331,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_key_last` through `BuiltinLoweringContext`.", @@ -5950,7 +6429,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_keys` through `BuiltinLoweringContext`.", @@ -6054,7 +6533,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_map` through `BuiltinLoweringContext`.", @@ -6169,7 +6648,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_merge` through `BuiltinLoweringContext`.", @@ -6243,7 +6722,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_merge_recursive` through `BuiltinLoweringContext`.", @@ -6317,7 +6796,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_multisort` through `BuiltinLoweringContext`.", @@ -6451,7 +6930,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_pad` through `BuiltinLoweringContext`.", @@ -6471,7 +6950,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -6562,7 +7043,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_pop` through `BuiltinLoweringContext`.", @@ -6677,7 +7158,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_product` through `BuiltinLoweringContext`.", @@ -6774,7 +7255,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_push` through `BuiltinLoweringContext`.", @@ -6889,7 +7370,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_rand` through `BuiltinLoweringContext`.", @@ -7016,7 +7497,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_reduce` through `BuiltinLoweringContext`.", @@ -7129,7 +7610,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_replace` through `BuiltinLoweringContext`.", @@ -7218,7 +7699,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_replace_recursive` through `BuiltinLoweringContext`.", @@ -7329,7 +7810,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_reverse` through `BuiltinLoweringContext`.", @@ -7386,6 +7867,13 @@ "name": "array", "optional": false, "type": "array" + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true, + "type": "bool" } ], "return_type": "array", @@ -7439,7 +7927,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_search` through `BuiltinLoweringContext`.", @@ -7550,7 +8038,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_shift` through `BuiltinLoweringContext`.", @@ -7662,6 +8150,12 @@ "default": "null", "name": "length", "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true } ], "required_param_count": 2, @@ -7677,7 +8171,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_slice` through `BuiltinLoweringContext`.", @@ -7711,7 +8205,7 @@ "kind": "static", "values": [] }, - "result_type": "shared", + "result_type": "checked", "runtime_functions": [ "array_slice" ], @@ -7748,6 +8242,13 @@ "name": "length", "optional": true, "type": "int" + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true, + "type": "bool" } ], "return_type": "array", @@ -7806,7 +8307,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_splice` through `BuiltinLoweringContext`.", @@ -7819,7 +8320,7 @@ }, "name": "array_splice", "semantics": { - "argument_lowering": "standard", + "argument_lowering": "array_splice", "callable": { "kind": "static_only", "reason": "typed backend operation has no runtime-selected wrapper contract" @@ -7851,7 +8352,7 @@ }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -7894,6 +8395,13 @@ "name": "length", "optional": true, "type": "int" + }, + { + "by_ref": false, + "default": "[]", + "name": "replacement", + "optional": true, + "type": "array" } ], "return_type": "array", @@ -7935,7 +8443,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_sum` through `BuiltinLoweringContext`.", @@ -8017,7 +8525,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_udiff` through `BuiltinLoweringContext`.", @@ -8130,7 +8638,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_uintersect` through `BuiltinLoweringContext`.", @@ -8259,7 +8767,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_unique` through `BuiltinLoweringContext`.", @@ -8356,7 +8864,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_unshift` through `BuiltinLoweringContext`.", @@ -8471,7 +8979,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_values` through `BuiltinLoweringContext`.", @@ -8574,7 +9082,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_walk` through `BuiltinLoweringContext`.", @@ -8680,7 +9188,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.array_walk_recursive` through `BuiltinLoweringContext`.", @@ -8801,7 +9309,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.arsort` through `BuiltinLoweringContext`.", @@ -8916,7 +9424,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.asin` through `BuiltinLoweringContext`.", @@ -9012,7 +9520,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.asort` through `BuiltinLoweringContext`.", @@ -9127,7 +9635,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.atan` through `BuiltinLoweringContext`.", @@ -9230,7 +9738,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.atan2` through `BuiltinLoweringContext`.", @@ -9319,6 +9827,12 @@ "default": null, "name": "string", "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true } ], "required_param_count": 1, @@ -9334,10 +9848,10 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.string.base64_decode` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.base64_decode` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], @@ -9346,6 +9860,111 @@ "sig_line": null }, "name": "base64_decode", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "base64_decode" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "base64_decode" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true, + "type": "bool" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "base64_decode", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "base64_encode", + "description": "Encodes binary data into a Base64 string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.string.base64_encode` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/base64_encode.rs", + "sig_line": null + }, + "name": "base64_encode", "semantics": { "argument_lowering": "standard", "callable": { @@ -9357,7 +9976,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "string.base64_decode" + "target": "string.base64_encode" }, "ownership": { "argument_indexes": [], @@ -9392,16 +10011,16 @@ "return_type": "string", "variadic": null }, - "slug": "base64_decode", + "slug": "base64_encode", "sub_area": "String" }, { - "area": "String", - "canonical_name": "base64_encode", - "description": "Encodes binary data into a Base64 string.", + "area": "Math", + "canonical_name": "base_convert", + "description": "Converts a number between two arbitrary bases from 2 to 36.", "eval": { - "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs", + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/base_convert.rs", "hooks": [ "direct", "values" @@ -9411,11 +10030,23 @@ { "by_ref": false, "default": null, - "name": "string", + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "from_base", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to_base", "optional": false } ], - "required_param_count": 1, + "required_param_count": 3, "supported": true, "variadic": null }, @@ -9428,41 +10059,46 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.string.base64_encode` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.base_convert` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/base64_encode.rs", + "sig_file": "src/builtins/math/base_convert.rs", "sig_line": null }, - "name": "base64_encode", + "name": "base_convert", "semantics": { "argument_lowering": "standard", "callable": { - "kind": "dynamic" + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", - "target": "string.base64_encode" + "target": "base_convert" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "independent" }, "requirements": { "kind": "static", "values": [] }, "result_type": "declared", - "runtime_functions": [], + "runtime_functions": [ + "base_convert" + ], "target_strategy": "runtime_call", "target_support": [ "macos-aarch64", @@ -9478,16 +10114,30 @@ { "by_ref": false, "default": null, - "name": "string", + "name": "num", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "from_base", + "optional": false, + "type": "int" + }, + { + "by_ref": false, + "default": null, + "name": "to_base", + "optional": false, + "type": "int" } ], "return_type": "string", "variadic": null }, - "slug": "base64_encode", - "sub_area": "String" + "slug": "base_convert", + "sub_area": "Math" }, { "area": "Filesystem", @@ -9528,7 +10178,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.basename` through `BuiltinLoweringContext`.", @@ -9649,7 +10299,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.bin_to_hex` through `BuiltinLoweringContext`.", @@ -9710,6 +10360,88 @@ "slug": "bin2hex", "sub_area": "String" }, + { + "area": "Math", + "canonical_name": "bindec", + "description": "Converts a binary string to its decimal number.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.bindec` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/bindec.rs", + "sig_line": null + }, + "name": "bindec", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "bindec" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "bindec" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "binary_string", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "bindec", + "sub_area": "Math" + }, { "area": "Type", "canonical_name": "boolval", @@ -9743,7 +10475,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -9834,7 +10566,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.buffer_free` through `BuiltinLoweringContext`.", @@ -9949,7 +10681,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.buffer_len` through `BuiltinLoweringContext`.", @@ -10108,7 +10840,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.call_user_func` through `BuiltinLoweringContext`.", @@ -10229,7 +10961,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.call_user_func_array` through `BuiltinLoweringContext`.", @@ -10351,7 +11083,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ceil` through `BuiltinLoweringContext`.", @@ -10448,7 +11180,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chdir` through `BuiltinLoweringContext`.", @@ -10574,7 +11306,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.checkdate` through `BuiltinLoweringContext`.", @@ -10708,7 +11440,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chgrp` through `BuiltinLoweringContext`.", @@ -10836,7 +11568,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chmod` through `BuiltinLoweringContext`.", @@ -10964,7 +11696,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chop` through `BuiltinLoweringContext`.", @@ -11074,7 +11806,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chown` through `BuiltinLoweringContext`.", @@ -11196,7 +11928,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.chr` through `BuiltinLoweringContext`.", @@ -11261,12 +11993,12 @@ "sub_area": "String" }, { - "area": "Math", - "canonical_name": "clamp", - "description": "Clamps a value to be within a specified range.", + "area": "String", + "canonical_name": "chunk_split", + "description": "Splits a string into fixed-length chunks separated by a given string.", "eval": { - "area": "math", - "home_file": "crates/elephc-magician/src/interpreter/builtins/math/clamp.rs", + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/chunk_split.rs", "hooks": [ "direct", "values" @@ -11276,23 +12008,23 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "string", "optional": false }, { "by_ref": false, - "default": null, - "name": "min", - "optional": false + "default": "76", + "name": "length", + "optional": true }, { "by_ref": false, - "default": null, - "name": "max", - "optional": false + "default": "\"\\r\\n\"", + "name": "separator", + "optional": true } ], - "required_param_count": 3, + "required_param_count": 1, "supported": true, "variadic": null }, @@ -11305,18 +12037,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.clamp` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.chunk_split` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/math/clamp.rs", + "sig_file": "src/builtins/string/chunk_split.rs", "sig_line": null }, - "name": "clamp", + "name": "chunk_split", "semantics": { "argument_lowering": "standard", "callable": { @@ -11331,19 +12063,19 @@ }, "lowering": { "kind": "runtime_call", - "target": "clamp" + "target": "chunk_split" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "independent" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "shared", + "result_type": "declared", "runtime_functions": [ - "clamp" + "chunk_split" ], "target_strategy": "runtime_call", "target_support": [ @@ -11360,38 +12092,38 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "string", "optional": false, - "type": "int" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "min", - "optional": false, + "default": "76", + "name": "length", + "optional": true, "type": "int" }, { "by_ref": false, - "default": null, - "name": "max", - "optional": false, - "type": "int" + "default": "'\\r\\n'", + "name": "separator", + "optional": true, + "type": "string" } ], - "return_type": "mixed", + "return_type": "string", "variadic": null }, - "slug": "clamp", - "sub_area": "Math" + "slug": "chunk_split", + "sub_area": "String" }, { - "area": "Class", - "canonical_name": "class_alias", - "description": "Creates an alias for a class.", + "area": "Math", + "canonical_name": "clamp", + "description": "Clamps a value to be within a specified range.", "eval": { - "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs", + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/clamp.rs", "hooks": [ "direct", "values" @@ -11401,23 +12133,23 @@ { "by_ref": false, "default": null, - "name": "class", + "name": "value", "optional": false }, { "by_ref": false, "default": null, - "name": "alias", + "name": "min", "optional": false }, { "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true + "default": null, + "name": "max", + "optional": false } ], - "required_param_count": 2, + "required_param_count": 3, "supported": true, "variadic": null }, @@ -11430,18 +12162,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_alias` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.clamp` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/class_alias.rs", + "sig_file": "src/builtins/math/clamp.rs", "sig_line": null }, - "name": "class_alias", + "name": "clamp", "semantics": { "argument_lowering": "standard", "callable": { @@ -11451,27 +12183,12 @@ "effects": { "kind": "static", "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" + "may_throw" ] }, "lowering": { "kind": "runtime_call", - "target": "class_alias" + "target": "clamp" }, "ownership": { "argument_indexes": [], @@ -11481,9 +12198,9 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "shared", "runtime_functions": [ - "class_alias" + "clamp" ], "target_strategy": "runtime_call", "target_support": [ @@ -11492,8 +12209,7 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { @@ -11501,38 +12217,38 @@ { "by_ref": false, "default": null, - "name": "class", + "name": "value", "optional": false, - "type": "string" + "type": "int" }, { "by_ref": false, "default": null, - "name": "alias", + "name": "min", "optional": false, - "type": "string" + "type": "int" }, { "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true, - "type": "bool" + "default": null, + "name": "max", + "optional": false, + "type": "int" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "class_alias", - "sub_area": "Class" + "slug": "clamp", + "sub_area": "Math" }, { "area": "Class", - "canonical_name": "class_attribute_args", - "description": "Returns the constructor arguments of a named attribute applied to a class.", + "canonical_name": "class_alias", + "description": "Creates an alias for a class.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs", "hooks": [ "direct", "values" @@ -11542,14 +12258,20 @@ { "by_ref": false, "default": null, - "name": "class_name", + "name": "class", "optional": false }, { "by_ref": false, "default": null, - "name": "attribute_name", + "name": "alias", "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true } ], "required_param_count": 2, @@ -11558,25 +12280,25 @@ }, "eval_only": false, "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_attribute_args` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.class_alias` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/class_attribute_args.rs", + "sig_file": "src/builtins/callables/class_alias.rs", "sig_line": null }, - "name": "class_attribute_args", + "name": "class_alias", "semantics": { "argument_lowering": "standard", "callable": { @@ -11606,7 +12328,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "class_attribute_args" + "target": "class_alias" }, "ownership": { "argument_indexes": [], @@ -11616,9 +12338,9 @@ "kind": "static", "values": [] }, - "result_type": "shared", + "result_type": "checked", "runtime_functions": [ - "class_attribute_args" + "class_alias" ], "target_strategy": "runtime_call", "target_support": [ @@ -11636,31 +12358,38 @@ { "by_ref": false, "default": null, - "name": "class_name", + "name": "class", "optional": false, "type": "string" }, { "by_ref": false, "default": null, - "name": "attribute_name", + "name": "alias", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "class_attribute_args", - "sub_area": "Attributes" + "slug": "class_alias", + "sub_area": "Class" }, { "area": "Class", - "canonical_name": "class_attribute_names", - "description": "Returns the list of attribute names applied to a class.", + "canonical_name": "class_attribute_args", + "description": "Returns the constructor arguments of a named attribute applied to a class.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs", "hooks": [ "direct", "values" @@ -11672,9 +12401,15 @@ "default": null, "name": "class_name", "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "attribute_name", + "optional": false } ], - "required_param_count": 1, + "required_param_count": 2, "supported": true, "variadic": null }, @@ -11687,18 +12422,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_attribute_names` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.class_attribute_args` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/class_attribute_names.rs", + "sig_file": "src/builtins/system/class_attribute_args.rs", "sig_line": null }, - "name": "class_attribute_names", + "name": "class_attribute_args", "semantics": { "argument_lowering": "standard", "callable": { @@ -11728,7 +12463,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "class_attribute_names" + "target": "class_attribute_args" }, "ownership": { "argument_indexes": [], @@ -11738,9 +12473,9 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "shared", "runtime_functions": [ - "class_attribute_names" + "class_attribute_args" ], "target_strategy": "runtime_call", "target_support": [ @@ -11761,149 +12496,28 @@ "name": "class_name", "optional": false, "type": "string" - } - ], - "return_type": "array", - "variadic": null - }, - "slug": "class_attribute_names", - "sub_area": "Attributes" - }, - { - "area": "Class", - "canonical_name": "class_exists", - "description": "Checks whether the given class has been defined.", - "eval": { - "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [ - { - "by_ref": false, - "default": null, - "name": "class", - "optional": false }, - { - "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true - } - ], - "required_param_count": 1, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_exists` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/callables/class_exists.rs", - "sig_line": null - }, - "name": "class_exists", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "class_exists" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "checked", - "runtime_functions": [ - "class_exists" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "checker_hook", - "lazy": false - } - }, - "sig": { - "params": [ { "by_ref": false, "default": null, - "name": "class", + "name": "attribute_name", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true, - "type": "bool" } ], - "return_type": "bool", + "return_type": "array", "variadic": null }, - "slug": "class_exists", - "sub_area": "Class" + "slug": "class_attribute_args", + "sub_area": "Attributes" }, { "area": "Class", - "canonical_name": "class_get_attributes", - "description": "Returns an array of ReflectionAttribute objects for all attributes of a class.", + "canonical_name": "class_attribute_names", + "description": "Returns the list of attribute names applied to a class.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs", "hooks": [ "direct", "values" @@ -11930,18 +12544,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_get_attributes` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.class_attribute_names` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/class_get_attributes.rs", + "sig_file": "src/builtins/system/class_attribute_names.rs", "sig_line": null }, - "name": "class_get_attributes", + "name": "class_attribute_names", "semantics": { "argument_lowering": "standard", "callable": { @@ -11971,7 +12585,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "class_get_attributes" + "target": "class_attribute_names" }, "ownership": { "argument_indexes": [], @@ -11983,7 +12597,7 @@ }, "result_type": "checked", "runtime_functions": [ - "class_get_attributes" + "class_attribute_names" ], "target_strategy": "runtime_call", "target_support": [ @@ -12009,16 +12623,16 @@ "return_type": "array", "variadic": null }, - "slug": "class_get_attributes", + "slug": "class_attribute_names", "sub_area": "Attributes" }, { "area": "Class", - "canonical_name": "class_implements", - "description": "Returns the interfaces which are implemented by the given class or its parents.", + "canonical_name": "class_exists", + "description": "Checks whether the given class has been defined.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs", "hooks": [ "direct", "values" @@ -12028,7 +12642,7 @@ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "class", "optional": false }, { @@ -12051,18 +12665,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_implements` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.class_exists` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/class_implements.rs", + "sig_file": "src/builtins/callables/class_exists.rs", "sig_line": null }, - "name": "class_implements", + "name": "class_exists", "semantics": { "argument_lowering": "standard", "callable": { @@ -12092,7 +12706,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "class_implements" + "target": "class_exists" }, "ownership": { "argument_indexes": [], @@ -12104,7 +12718,7 @@ }, "result_type": "checked", "runtime_functions": [ - "class_implements" + "class_exists" ], "target_strategy": "runtime_call", "target_support": [ @@ -12114,7 +12728,7 @@ ], "validation": { "kind": "checker_hook", - "lazy": true + "lazy": false } }, "sig": { @@ -12122,9 +12736,9 @@ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "class", "optional": false, - "type": "mixed" + "type": "string" }, { "by_ref": false, @@ -12134,19 +12748,19 @@ "type": "bool" } ], - "return_type": "mixed", + "return_type": "bool", "variadic": null }, - "slug": "class_implements", + "slug": "class_exists", "sub_area": "Class" }, { "area": "Class", - "canonical_name": "class_parents", - "description": "Returns the parent classes of the given class.", + "canonical_name": "class_get_attributes", + "description": "Returns an array of ReflectionAttribute objects for all attributes of a class.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs", "hooks": [ "direct", "values" @@ -12156,14 +12770,8 @@ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "class_name", "optional": false - }, - { - "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true } ], "required_param_count": 1, @@ -12172,25 +12780,25 @@ }, "eval_only": false, "in_catalog": true, - "is_extension": false, + "is_extension": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.class_parents` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.class_get_attributes` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/class_parents.rs", + "sig_file": "src/builtins/system/class_get_attributes.rs", "sig_line": null }, - "name": "class_parents", + "name": "class_get_attributes", "semantics": { "argument_lowering": "standard", "callable": { @@ -12220,7 +12828,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "class_parents" + "target": "class_get_attributes" }, "ownership": { "argument_indexes": [], @@ -12232,7 +12840,7 @@ }, "result_type": "checked", "runtime_functions": [ - "class_parents" + "class_get_attributes" ], "target_strategy": "runtime_call", "target_support": [ @@ -12242,7 +12850,7 @@ ], "validation": { "kind": "checker_hook", - "lazy": true + "lazy": false } }, "sig": { @@ -12250,31 +12858,24 @@ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "class_name", "optional": false, - "type": "mixed" - }, - { - "by_ref": false, - "default": "true", - "name": "autoload", - "optional": true, - "type": "bool" + "type": "string" } ], - "return_type": "mixed", + "return_type": "array", "variadic": null }, - "slug": "class_parents", - "sub_area": "Class" + "slug": "class_get_attributes", + "sub_area": "Attributes" }, { "area": "Class", - "canonical_name": "class_uses", - "description": "Returns the traits used by the given class.", + "canonical_name": "class_implements", + "description": "Returns the interfaces which are implemented by the given class or its parents.", "eval": { "area": "symbols", - "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs", "hooks": [ "direct", "values" @@ -12307,7 +12908,263 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.class_implements` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/class_implements.rs", + "sig_line": null + }, + "name": "class_implements", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "class_implements" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "class_implements" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": true + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "class_implements", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "class_parents", + "description": "Returns the parent classes of the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.class_parents` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/class_parents.rs", + "sig_line": null + }, + "name": "class_parents", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "class_parents" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "class_parents" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": true + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true, + "type": "bool" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "class_parents", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "class_uses", + "description": "Returns the traits used by the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.class_uses` through `BuiltinLoweringContext`.", @@ -12435,7 +13292,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.clearstatcache` through `BuiltinLoweringContext`.", @@ -12556,7 +13413,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.closedir` through `BuiltinLoweringContext`.", @@ -12638,6 +13495,102 @@ "slug": "closedir", "sub_area": "IO" }, + { + "area": "Misc", + "canonical_name": "constant", + "description": "Returns the value of a constant given its name.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/constant.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", + "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/constant.rs", + "sig_line": null + }, + "name": "constant", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "constant() needs a compile-time constant name" + }, + "effects": { + "kind": "static", + "names": [ + "reads_global" + ] + }, + "lowering": { + "kind": "eir" + }, + "ownership": { + "argument_indexes": [], + "kind": "non_heap" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "eir_primitive", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "name", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "constant", + "sub_area": "Constants" + }, { "area": "Filesystem", "canonical_name": "copy", @@ -12677,7 +13630,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.copy` through `BuiltinLoweringContext`.", @@ -12798,7 +13751,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.cos` through `BuiltinLoweringContext`.", @@ -12895,7 +13848,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.cosh` through `BuiltinLoweringContext`.", @@ -12998,7 +13951,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.count` through `BuiltinLoweringContext`.", @@ -13071,11 +14024,11 @@ }, { "area": "String", - "canonical_name": "crc32", - "description": "Calculates the CRC32 polynomial of a string.", + "canonical_name": "count_chars", + "description": "Returns byte-frequency information about a string as a tally or a byte list.", "eval": { "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/crc32.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/count_chars.rs", "hooks": [ "direct", "values" @@ -13087,6 +14040,12 @@ "default": null, "name": "string", "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true } ], "required_param_count": 1, @@ -13102,18 +14061,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.crc32` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.count_chars` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/crc32.rs", + "sig_file": "src/builtins/string/count_chars.rs", "sig_line": null }, - "name": "crc32", + "name": "count_chars", "semantics": { "argument_lowering": "standard", "callable": { @@ -13122,23 +14081,25 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", - "target": "crc32" + "target": "count_chars" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ - "crc32" + "count_chars" ], "target_strategy": "runtime_call", "target_support": [ @@ -13147,7 +14108,8 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": false } }, "sig": { @@ -13158,21 +14120,28 @@ "name": "string", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true, + "type": "int" } ], - "return_type": "int", + "return_type": "array|string", "variadic": null }, - "slug": "crc32", + "slug": "count_chars", "sub_area": "String" }, { - "area": "Type", - "canonical_name": "ctype_alnum", - "description": "Checks if all characters in the string are alphanumeric.", + "area": "String", + "canonical_name": "crc32", + "description": "Calculates the CRC32 polynomial of a string.", "eval": { "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/crc32.rs", "hooks": [ "direct", "values" @@ -13182,7 +14151,7 @@ { "by_ref": false, "default": null, - "name": "text", + "name": "string", "optional": false } ], @@ -13199,18 +14168,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ctype_alnum` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.crc32` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/ctype_alnum.rs", + "sig_file": "src/builtins/string/crc32.rs", "sig_line": null }, - "name": "ctype_alnum", + "name": "crc32", "semantics": { "argument_lowering": "standard", "callable": { @@ -13223,7 +14192,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ctype_alnum" + "target": "crc32" }, "ownership": { "argument_indexes": [], @@ -13235,7 +14204,7 @@ }, "result_type": "declared", "runtime_functions": [ - "ctype_alnum" + "crc32" ], "target_strategy": "runtime_call", "target_support": [ @@ -13252,24 +14221,24 @@ { "by_ref": false, "default": null, - "name": "text", + "name": "string", "optional": false, "type": "string" } ], - "return_type": "bool", + "return_type": "int", "variadic": null }, - "slug": "ctype_alnum", - "sub_area": "Ctype" + "slug": "crc32", + "sub_area": "String" }, { "area": "Type", - "canonical_name": "ctype_alpha", - "description": "Checks if all characters in the string are alphabetic.", + "canonical_name": "ctype_alnum", + "description": "Checks if all characters in the string are alphanumeric.", "eval": { "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs", "hooks": [ "direct", "values" @@ -13296,18 +14265,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ctype_alpha` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ctype_alnum` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/ctype_alpha.rs", + "sig_file": "src/builtins/string/ctype_alnum.rs", "sig_line": null }, - "name": "ctype_alpha", + "name": "ctype_alnum", "semantics": { "argument_lowering": "standard", "callable": { @@ -13320,7 +14289,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ctype_alpha" + "target": "ctype_alnum" }, "ownership": { "argument_indexes": [], @@ -13332,7 +14301,7 @@ }, "result_type": "declared", "runtime_functions": [ - "ctype_alpha" + "ctype_alnum" ], "target_strategy": "runtime_call", "target_support": [ @@ -13357,16 +14326,16 @@ "return_type": "bool", "variadic": null }, - "slug": "ctype_alpha", + "slug": "ctype_alnum", "sub_area": "Ctype" }, { "area": "Type", - "canonical_name": "ctype_digit", - "description": "Checks if all characters in the string are digits.", + "canonical_name": "ctype_alpha", + "description": "Checks if all characters in the string are alphabetic.", "eval": { "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs", "hooks": [ "direct", "values" @@ -13393,18 +14362,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ctype_digit` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ctype_alpha` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/ctype_digit.rs", + "sig_file": "src/builtins/string/ctype_alpha.rs", "sig_line": null }, - "name": "ctype_digit", + "name": "ctype_alpha", "semantics": { "argument_lowering": "standard", "callable": { @@ -13417,7 +14386,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ctype_digit" + "target": "ctype_alpha" }, "ownership": { "argument_indexes": [], @@ -13429,7 +14398,7 @@ }, "result_type": "declared", "runtime_functions": [ - "ctype_digit" + "ctype_alpha" ], "target_strategy": "runtime_call", "target_support": [ @@ -13454,16 +14423,16 @@ "return_type": "bool", "variadic": null }, - "slug": "ctype_digit", + "slug": "ctype_alpha", "sub_area": "Ctype" }, { "area": "Type", - "canonical_name": "ctype_space", - "description": "Checks if all characters in the string are whitespace characters.", + "canonical_name": "ctype_digit", + "description": "Checks if all characters in the string are digits.", "eval": { "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs", "hooks": [ "direct", "values" @@ -13490,18 +14459,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ctype_space` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ctype_digit` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/string/ctype_space.rs", + "sig_file": "src/builtins/string/ctype_digit.rs", "sig_line": null }, - "name": "ctype_space", + "name": "ctype_digit", "semantics": { "argument_lowering": "standard", "callable": { @@ -13514,7 +14483,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ctype_space" + "target": "ctype_digit" }, "ownership": { "argument_indexes": [], @@ -13526,7 +14495,7 @@ }, "result_type": "declared", "runtime_functions": [ - "ctype_space" + "ctype_digit" ], "target_strategy": "runtime_call", "target_support": [ @@ -13551,16 +14520,16 @@ "return_type": "bool", "variadic": null }, - "slug": "ctype_space", + "slug": "ctype_digit", "sub_area": "Ctype" }, { - "area": "Date", - "canonical_name": "date", - "description": "Formats a local time/date.", + "area": "Type", + "canonical_name": "ctype_space", + "description": "Checks if all characters in the string are whitespace characters.", "eval": { - "area": "time", - "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date.rs", + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs", "hooks": [ "direct", "values" @@ -13570,14 +14539,8 @@ { "by_ref": false, "default": null, - "name": "format", + "name": "text", "optional": false - }, - { - "by_ref": false, - "default": "null", - "name": "timestamp", - "optional": true } ], "required_param_count": 1, @@ -13593,48 +14556,31 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.date` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ctype_space` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/date.rs", + "sig_file": "src/builtins/string/ctype_space.rs", "sig_line": null }, - "name": "date", + "name": "ctype_space", "semantics": { - "argument_lowering": "date", + "argument_lowering": "standard", "callable": { "kind": "static_only", "reason": "typed backend operation has no runtime-selected wrapper contract" }, "effects": { "kind": "static", - "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" - ] + "names": [] }, "lowering": { "kind": "runtime_call", - "target": "date" + "target": "ctype_space" }, "ownership": { "argument_indexes": [], @@ -13646,7 +14592,7 @@ }, "result_type": "declared", "runtime_functions": [ - "date" + "ctype_space" ], "target_strategy": "runtime_call", "target_support": [ @@ -13663,115 +14609,24 @@ { "by_ref": false, "default": null, - "name": "format", + "name": "text", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": "null", - "name": "timestamp", - "optional": true, - "type": "int" } ], - "return_type": "string", - "variadic": null - }, - "slug": "date", - "sub_area": "Date" - }, - { - "area": "Date", - "canonical_name": "date_default_timezone_get", - "description": "Gets the default timezone.", - "eval": { - "area": "time", - "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [], - "required_param_count": 0, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.date_default_timezone_get` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/system/date_default_timezone_get.rs", - "sig_line": null - }, - "name": "date_default_timezone_get", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_global" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "date_default_timezone_get" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "declared", - "runtime_functions": [ - "date_default_timezone_get" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "signature" - } - }, - "sig": { - "params": [], - "return_type": "string", + "return_type": "bool", "variadic": null }, - "slug": "date_default_timezone_get", - "sub_area": "Date" + "slug": "ctype_space", + "sub_area": "Ctype" }, { - "area": "Date", - "canonical_name": "date_default_timezone_set", - "description": "Sets the default timezone.", + "area": "Array", + "canonical_name": "current", + "description": "Returns the element under the array's internal pointer.", "eval": { - "area": "time", - "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs", + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/current.rs", "hooks": [ "direct", "values" @@ -13781,7 +14636,7 @@ { "by_ref": false, "default": null, - "name": "timezoneId", + "name": "array", "optional": false } ], @@ -13798,23 +14653,347 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.date_default_timezone_set` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.array_ptr_value` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/date_default_timezone_set.rs", + "sig_file": "src/builtins/array/current.rs", "sig_line": null }, - "name": "date_default_timezone_set", + "name": "current", "semantics": { - "argument_lowering": "standard", + "argument_lowering": "array_internal_pointer", "callable": { "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" + "reason": "the internal array pointer needs a named array variable receiver" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "array_ptr_value" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "current", + "sub_area": "Array" + }, + { + "area": "Date", + "canonical_name": "date", + "description": "Formats a local time/date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.date` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/date.rs", + "sig_line": null + }, + "name": "date", + "semantics": { + "argument_lowering": "date", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "date" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "date" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "date", + "sub_area": "Date" + }, + { + "area": "Date", + "canonical_name": "date_default_timezone_get", + "description": "Gets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.date_default_timezone_get` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/date_default_timezone_get.rs", + "sig_line": null + }, + "name": "date_default_timezone_get", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_global" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "date_default_timezone_get" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "date_default_timezone_get" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [], + "return_type": "string", + "variadic": null + }, + "slug": "date_default_timezone_get", + "sub_area": "Date" + }, + { + "area": "Date", + "canonical_name": "date_default_timezone_set", + "description": "Sets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "timezoneId", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.date_default_timezone_set` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/date_default_timezone_set.rs", + "sig_line": null + }, + "name": "date_default_timezone_set", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" }, "effects": { "kind": "static", @@ -13879,6 +15058,249 @@ "slug": "date_default_timezone_set", "sub_area": "Date" }, + { + "area": "Math", + "canonical_name": "decbin", + "description": "Converts an integer to its binary string representation.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.decbin` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/decbin.rs", + "sig_line": null + }, + "name": "decbin", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "decbin" + }, + "ownership": { + "argument_indexes": [], + "kind": "independent" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "decbin" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "decbin", + "sub_area": "Math" + }, + { + "area": "Math", + "canonical_name": "dechex", + "description": "Converts an integer to its hexadecimal string representation.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.dechex` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/dechex.rs", + "sig_line": null + }, + "name": "dechex", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "dechex" + }, + "ownership": { + "argument_indexes": [], + "kind": "independent" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "dechex" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "dechex", + "sub_area": "Math" + }, + { + "area": "Math", + "canonical_name": "decoct", + "description": "Converts an integer to its octal string representation.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.decoct` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/decoct.rs", + "sig_line": null + }, + "name": "decoct", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "decoct" + }, + "ownership": { + "argument_indexes": [], + "kind": "independent" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "decoct" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "decoct", + "sub_area": "Math" + }, { "area": "Misc", "canonical_name": "define", @@ -13918,7 +15340,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.define` through `BuiltinLoweringContext`.", @@ -14040,7 +15462,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.defined` through `BuiltinLoweringContext`.", @@ -14140,7 +15562,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.deg2rad` through `BuiltinLoweringContext`.", @@ -14301,7 +15723,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.dirname` through `BuiltinLoweringContext`.", @@ -14423,7 +15845,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.disk_free_space` through `BuiltinLoweringContext`.", @@ -14537,7 +15959,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.disk_total_space` through `BuiltinLoweringContext`.", @@ -14651,7 +16073,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/count_empty.rs", "codegen_function": "lower_empty", - "codegen_line": 88, + "codegen_line": 92, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -14680,6 +16102,118 @@ "slug": "empty", "sub_area": "Variable" }, + { + "area": "Array", + "canonical_name": "end", + "description": "Moves the array's internal pointer to the last element and returns it.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/end.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/array/end.rs", + "sig_line": null + }, + "name": "end", + "semantics": { + "argument_lowering": "array_internal_pointer", + "callable": { + "kind": "static_only", + "reason": "the internal array pointer needs a named array variable receiver" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "array_ptr_seek" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "end", + "sub_area": "Array" + }, { "area": "Class", "canonical_name": "enum_exists", @@ -14719,7 +16253,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.enum_exists` through `BuiltinLoweringContext`.", @@ -14841,7 +16375,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.exec` through `BuiltinLoweringContext`.", @@ -15013,7 +16547,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.exp` through `BuiltinLoweringContext`.", @@ -15122,7 +16656,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.explode` through `BuiltinLoweringContext`.", @@ -15142,7 +16676,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -15234,7 +16770,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.extension_loaded` through `BuiltinLoweringContext`.", @@ -15349,7 +16885,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fclose` through `BuiltinLoweringContext`.", @@ -15464,7 +17000,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fdatasync` through `BuiltinLoweringContext`.", @@ -15585,7 +17121,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fdiv` through `BuiltinLoweringContext`.", @@ -15689,7 +17225,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.feof` through `BuiltinLoweringContext`.", @@ -15804,7 +17340,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fflush` through `BuiltinLoweringContext`.", @@ -15919,7 +17455,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgetc` through `BuiltinLoweringContext`.", @@ -16046,7 +17582,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgetcsv` through `BuiltinLoweringContext`.", @@ -16175,7 +17711,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fgets` through `BuiltinLoweringContext`.", @@ -16275,6 +17811,12 @@ "default": null, "name": "filename", "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true } ], "required_param_count": 1, @@ -16290,7 +17832,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file` through `BuiltinLoweringContext`.", @@ -16364,6 +17906,13 @@ "name": "filename", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" } ], "return_type": "array", @@ -16405,7 +17954,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_exists` through `BuiltinLoweringContext`.", @@ -16504,6 +18053,30 @@ "default": null, "name": "filename", "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "use_include_path", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "context", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true } ], "required_param_count": 1, @@ -16519,7 +18092,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_get_contents` through `BuiltinLoweringContext`.", @@ -16592,6 +18165,34 @@ "name": "filename", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "false", + "name": "use_include_path", + "optional": true, + "type": "bool" + }, + { + "by_ref": false, + "default": "null", + "name": "context", + "optional": true, + "type": "mixed" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "int" } ], "return_type": "mixed", @@ -16639,7 +18240,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.file_put_contents` through `BuiltinLoweringContext`.", @@ -16760,7 +18361,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileatime` through `BuiltinLoweringContext`.", @@ -16875,7 +18476,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filectime` through `BuiltinLoweringContext`.", @@ -16990,7 +18591,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filegroup` through `BuiltinLoweringContext`.", @@ -17105,7 +18706,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileinode` through `BuiltinLoweringContext`.", @@ -17220,7 +18821,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filemtime` through `BuiltinLoweringContext`.", @@ -17334,7 +18935,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileowner` through `BuiltinLoweringContext`.", @@ -17449,7 +19050,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fileperms` through `BuiltinLoweringContext`.", @@ -17564,7 +19165,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filesize` through `BuiltinLoweringContext`.", @@ -17678,7 +19279,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.filetype` through `BuiltinLoweringContext`.", @@ -17793,7 +19394,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -17895,7 +19496,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.flock` through `BuiltinLoweringContext`.", @@ -18024,7 +19625,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.floor` through `BuiltinLoweringContext`.", @@ -18127,7 +19728,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fmod` through `BuiltinLoweringContext`.", @@ -18243,7 +19844,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fnmatch` through `BuiltinLoweringContext`.", @@ -18390,7 +19991,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fopen` through `BuiltinLoweringContext`.", @@ -18525,7 +20126,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fpassthru` through `BuiltinLoweringContext`.", @@ -18646,7 +20247,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fprintf` through `BuiltinLoweringContext`.", @@ -18786,7 +20387,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fputcsv` through `BuiltinLoweringContext`.", @@ -18928,7 +20529,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fread` through `BuiltinLoweringContext`.", @@ -19056,7 +20657,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fscanf` through `BuiltinLoweringContext`.", @@ -19190,7 +20791,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fseek` through `BuiltinLoweringContext`.", @@ -19342,7 +20943,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fsockopen` through `BuiltinLoweringContext`.", @@ -19485,7 +21086,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fstat` through `BuiltinLoweringContext`.", @@ -19600,7 +21201,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fsync` through `BuiltinLoweringContext`.", @@ -19715,7 +21316,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ftell` through `BuiltinLoweringContext`.", @@ -19836,7 +21437,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ftruncate` through `BuiltinLoweringContext`.", @@ -19958,7 +21559,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.function_exists` through `BuiltinLoweringContext`.", @@ -20064,7 +21665,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.fwrite` through `BuiltinLoweringContext`.", @@ -20229,7 +21830,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_class` through `BuiltinLoweringContext`.", @@ -20437,7 +22038,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_classes` through `BuiltinLoweringContext`.", @@ -20537,7 +22138,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_interfaces` through `BuiltinLoweringContext`.", @@ -20637,7 +22238,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_declared_traits` through `BuiltinLoweringContext`.", @@ -20744,7 +22345,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_loaded_extensions` through `BuiltinLoweringContext`.", @@ -20917,7 +22518,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_parent_class` through `BuiltinLoweringContext`.", @@ -21016,7 +22617,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_resource_id` through `BuiltinLoweringContext`.", @@ -21113,7 +22714,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.get_resource_type` through `BuiltinLoweringContext`.", @@ -21203,7 +22804,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getcwd` through `BuiltinLoweringContext`.", @@ -21309,7 +22910,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getdate` through `BuiltinLoweringContext`.", @@ -21423,7 +23024,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getenv` through `BuiltinLoweringContext`.", @@ -21524,7 +23125,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostbyaddr` through `BuiltinLoweringContext`.", @@ -21639,7 +23240,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostbyname` through `BuiltinLoweringContext`.", @@ -21746,7 +23347,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gethostname` through `BuiltinLoweringContext`.", @@ -21838,7 +23439,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getprotobyname` through `BuiltinLoweringContext`.", @@ -21953,7 +23554,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getprotobynumber` through `BuiltinLoweringContext`.", @@ -22074,7 +23675,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getservbyname` through `BuiltinLoweringContext`.", @@ -22202,7 +23803,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.getservbyport` through `BuiltinLoweringContext`.", @@ -22324,7 +23925,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gettype` through `BuiltinLoweringContext`.", @@ -22421,7 +24022,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.glob` through `BuiltinLoweringContext`.", @@ -22542,7 +24143,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gmdate` through `BuiltinLoweringContext`.", @@ -22693,7 +24294,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gmmktime` through `BuiltinLoweringContext`.", @@ -22842,7 +24443,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.grapheme_strrev` through `BuiltinLoweringContext`.", @@ -22946,7 +24547,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzcompress` through `BuiltinLoweringContext`.", @@ -23078,7 +24679,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzdeflate` through `BuiltinLoweringContext`.", @@ -23210,7 +24811,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzinflate` through `BuiltinLoweringContext`.", @@ -23343,7 +24944,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.gzuncompress` through `BuiltinLoweringContext`.", @@ -23482,7 +25083,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash` through `BuiltinLoweringContext`.", @@ -23608,7 +25209,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_algos` through `BuiltinLoweringContext`.", @@ -23762,7 +25363,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_equals` through `BuiltinLoweringContext`.", @@ -23878,7 +25479,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_file` through `BuiltinLoweringContext`.", @@ -24101,7 +25702,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hash_hmac` through `BuiltinLoweringContext`.", @@ -24408,7 +26009,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.header` through `BuiltinLoweringContext`.", @@ -24536,7 +26137,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.hex_to_bin` through `BuiltinLoweringContext`.", @@ -24599,6 +26200,88 @@ "slug": "hex2bin", "sub_area": "String" }, + { + "area": "Math", + "canonical_name": "hexdec", + "description": "Converts a hexadecimal string to its decimal number.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.hexdec` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/math/hexdec.rs", + "sig_line": null + }, + "name": "hexdec", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "hexdec" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "hexdec" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "hex_string", + "optional": false, + "type": "string" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "hexdec", + "sub_area": "Math" + }, { "area": "Date", "canonical_name": "hrtime", @@ -24632,7 +26315,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hrtime` through `BuiltinLoweringContext`.", @@ -24732,7 +26415,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.html_entity_decode` through `BuiltinLoweringContext`.", @@ -24838,7 +26521,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.htmlentities` through `BuiltinLoweringContext`.", @@ -24961,7 +26644,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.htmlspecialchars` through `BuiltinLoweringContext`.", @@ -25072,7 +26755,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.http_response_code` through `BuiltinLoweringContext`.", @@ -25192,7 +26875,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.hypot` through `BuiltinLoweringContext`.", @@ -25302,7 +26985,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.implode` through `BuiltinLoweringContext`.", @@ -25336,7 +27019,7 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "declared", "runtime_functions": [ "implode" ], @@ -25347,8 +27030,7 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { @@ -25419,7 +27101,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.in_array` through `BuiltinLoweringContext`.", @@ -25548,7 +27230,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.inet_ntop` through `BuiltinLoweringContext`.", @@ -25646,7 +27328,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.inet_pton` through `BuiltinLoweringContext`.", @@ -25750,7 +27432,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.intdiv` through `BuiltinLoweringContext`.", @@ -25877,7 +27559,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.interface_exists` through `BuiltinLoweringContext`.", @@ -25969,7 +27651,7 @@ { "area": "Type", "canonical_name": "intval", - "description": "Returns the integer value of a variable.", + "description": "Returns the integer value of a variable, optionally using a given base.", "eval": { "area": "types", "home_file": "crates/elephc-magician/src/interpreter/builtins/types/intval.rs", @@ -25984,6 +27666,12 @@ "default": null, "name": "value", "optional": false + }, + { + "by_ref": false, + "default": "10", + "name": "base", + "optional": true } ], "required_param_count": 1, @@ -25999,9 +27687,9 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ - "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", + "Uses the `eir_graph` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." ], "runtime_helpers": [], @@ -26030,8 +27718,10 @@ "values": [] }, "result_type": "declared", - "runtime_functions": [], - "target_strategy": "eir_primitive", + "runtime_functions": [ + "intval_base" + ], + "target_strategy": "eir_graph", "target_support": [ "macos-aarch64", "linux-aarch64", @@ -26049,6 +27739,13 @@ "name": "value", "optional": false, "type": "mixed" + }, + { + "by_ref": false, + "default": "10", + "name": "base", + "optional": true, + "type": "int" } ], "return_type": "int", @@ -26090,7 +27787,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ip2long` through `BuiltinLoweringContext`.", @@ -26200,7 +27897,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_a` through `BuiltinLoweringContext`.", @@ -26328,7 +28025,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26419,7 +28116,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26522,7 +28219,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_callable` through `BuiltinLoweringContext`.", @@ -26636,7 +28333,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_dir` through `BuiltinLoweringContext`.", @@ -26750,7 +28447,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -26841,7 +28538,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_executable` through `BuiltinLoweringContext`.", @@ -26955,7 +28652,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_file` through `BuiltinLoweringContext`.", @@ -27069,7 +28766,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_finite` through `BuiltinLoweringContext`.", @@ -27166,7 +28863,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27257,7 +28954,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_infinite` through `BuiltinLoweringContext`.", @@ -27354,7 +29051,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27445,7 +29142,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27536,7 +29233,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27627,7 +29324,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_link` through `BuiltinLoweringContext`.", @@ -27741,7 +29438,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -27832,7 +29529,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_nan` through `BuiltinLoweringContext`.", @@ -27929,7 +29626,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28021,7 +29718,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_numeric` through `BuiltinLoweringContext`.", @@ -28118,7 +29815,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28209,7 +29906,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_readable` through `BuiltinLoweringContext`.", @@ -28323,7 +30020,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28414,7 +30111,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28505,7 +30202,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28596,7 +30293,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -28699,7 +30396,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_subclass_of` through `BuiltinLoweringContext`.", @@ -28827,7 +30524,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_writable` through `BuiltinLoweringContext`.", @@ -28941,7 +30638,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.is_writeable` through `BuiltinLoweringContext`.", @@ -29127,7 +30824,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_apply` through `BuiltinLoweringContext`.", @@ -29256,7 +30953,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_count` through `BuiltinLoweringContext`.", @@ -29377,7 +31074,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.iterator_to_array` through `BuiltinLoweringContext`.", @@ -29466,6 +31163,94 @@ "slug": "iterator_to_array", "sub_area": "SPL" }, + { + "area": "String", + "canonical_name": "join", + "description": "Joins array elements into a single string using a separator (alias of implode).", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.implode` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/join.rs", + "sig_line": null + }, + "name": "join", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "implode" + }, + "ownership": { + "argument_indexes": [], + "kind": "independent" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "implode" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "separator", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": "null", + "name": "array", + "optional": true, + "type": "mixed" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "join", + "sub_area": "String" + }, { "area": "JSON", "canonical_name": "json_decode", @@ -29517,7 +31302,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_decode` through `BuiltinLoweringContext`.", @@ -29665,7 +31450,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.json_encode` through `BuiltinLoweringContext`.", @@ -29706,19 +31491,328 @@ }, "lowering": { "kind": "runtime_call", - "target": "json_encode" + "target": "json_encode" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "shared", + "runtime_functions": [ + "json_encode" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "json_encode", + "sub_area": "JSON" + }, + { + "area": "JSON", + "canonical_name": "json_last_error", + "description": "Returns the last error (if any) occurred during the last JSON encoding/decoding.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.json_last_error` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/json_last_error.rs", + "sig_line": null + }, + "name": "json_last_error", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_global" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "json_last_error" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "json_last_error" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [], + "return_type": "int", + "variadic": null + }, + "slug": "json_last_error", + "sub_area": "JSON" + }, + { + "area": "JSON", + "canonical_name": "json_last_error_msg", + "description": "Returns the error string of the last json_encode() or json_decode() call.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.json_last_error_msg` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/json_last_error_msg.rs", + "sig_line": null + }, + "name": "json_last_error_msg", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_global" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "json_last_error_msg" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "json_last_error_msg" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [], + "return_type": "string", + "variadic": null + }, + "slug": "json_last_error_msg", + "sub_area": "JSON" + }, + { + "area": "JSON", + "canonical_name": "json_validate", + "description": "Checks if a string contains valid JSON.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "json", + "optional": false + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.json_validate` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/system/json_validate.rs", + "sig_line": null + }, + "name": "json_validate", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "json_validate" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "shared", + "result_type": "checked", "runtime_functions": [ - "json_encode" + "json_validate" ], "target_strategy": "runtime_call", "target_support": [ @@ -29736,206 +31830,38 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "json", "optional": false, - "type": "mixed" + "type": "string" }, { "by_ref": false, - "default": "0", - "name": "flags", + "default": "512", + "name": "depth", "optional": true, "type": "int" }, { "by_ref": false, - "default": "512", - "name": "depth", + "default": "0", + "name": "flags", "optional": true, "type": "int" } ], - "return_type": "string", - "variadic": null - }, - "slug": "json_encode", - "sub_area": "JSON" - }, - { - "area": "JSON", - "canonical_name": "json_last_error", - "description": "Returns the last error (if any) occurred during the last JSON encoding/decoding.", - "eval": { - "area": "json", - "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [], - "required_param_count": 0, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.json_last_error` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/system/json_last_error.rs", - "sig_line": null - }, - "name": "json_last_error", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_global" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "json_last_error" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "declared", - "runtime_functions": [ - "json_last_error" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "signature" - } - }, - "sig": { - "params": [], - "return_type": "int", - "variadic": null - }, - "slug": "json_last_error", - "sub_area": "JSON" - }, - { - "area": "JSON", - "canonical_name": "json_last_error_msg", - "description": "Returns the error string of the last json_encode() or json_decode() call.", - "eval": { - "area": "json", - "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [], - "required_param_count": 0, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.json_last_error_msg` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/system/json_last_error_msg.rs", - "sig_line": null - }, - "name": "json_last_error_msg", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_global" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "json_last_error_msg" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "declared", - "runtime_functions": [ - "json_last_error_msg" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "signature" - } - }, - "sig": { - "params": [], - "return_type": "string", + "return_type": "bool", "variadic": null }, - "slug": "json_last_error_msg", + "slug": "json_validate", "sub_area": "JSON" }, { - "area": "JSON", - "canonical_name": "json_validate", - "description": "Checks if a string contains valid JSON.", + "area": "Array", + "canonical_name": "key", + "description": "Returns the key of the element under the array's internal pointer.", "eval": { - "area": "json", - "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs", + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/key.rs", "hooks": [ "direct", "values" @@ -29945,20 +31871,8 @@ { "by_ref": false, "default": null, - "name": "json", + "name": "array", "optional": false - }, - { - "by_ref": false, - "default": "512", - "name": "depth", - "optional": true - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true } ], "required_param_count": 1, @@ -29974,23 +31888,23 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.json_validate` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.array_ptr_key` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/json_validate.rs", + "sig_file": "src/builtins/array/key.rs", "sig_line": null }, - "name": "json_validate", + "name": "key", "semantics": { - "argument_lowering": "standard", + "argument_lowering": "array_internal_pointer", "callable": { "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" + "reason": "the internal array pointer needs a named array variable receiver" }, "effects": { "kind": "static", @@ -30015,20 +31929,18 @@ }, "lowering": { "kind": "runtime_call", - "target": "json_validate" + "target": "array_ptr_key" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, "result_type": "checked", - "runtime_functions": [ - "json_validate" - ], + "runtime_functions": [], "target_strategy": "runtime_call", "target_support": [ "macos-aarch64", @@ -30045,30 +31957,16 @@ { "by_ref": false, "default": null, - "name": "json", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": "512", - "name": "depth", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true, - "type": "int" + "type": "array" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "json_validate", - "sub_area": "JSON" + "slug": "key", + "sub_area": "Array" }, { "area": "Array", @@ -30102,7 +32000,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.krsort` through `BuiltinLoweringContext`.", @@ -30216,7 +32114,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ksort` through `BuiltinLoweringContext`.", @@ -30331,7 +32229,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lcfirst` through `BuiltinLoweringContext`.", @@ -30434,7 +32332,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lchgrp` through `BuiltinLoweringContext`.", @@ -30562,7 +32460,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lchown` through `BuiltinLoweringContext`.", @@ -30690,7 +32588,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.link` through `BuiltinLoweringContext`.", @@ -30811,7 +32709,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.linkinfo` through `BuiltinLoweringContext`.", @@ -30931,7 +32829,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.localtime` through `BuiltinLoweringContext`.", @@ -31058,7 +32956,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log` through `BuiltinLoweringContext`.", @@ -31162,7 +33060,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log10` through `BuiltinLoweringContext`.", @@ -31259,7 +33157,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.log2` through `BuiltinLoweringContext`.", @@ -31356,7 +33254,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.long2ip` through `BuiltinLoweringContext`.", @@ -31453,7 +33351,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.lstat` through `BuiltinLoweringContext`.", @@ -31574,7 +33472,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ltrim` through `BuiltinLoweringContext`.", @@ -31678,7 +33576,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.max` through `BuiltinLoweringContext`.", @@ -31698,7 +33596,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -31706,7 +33606,7 @@ }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -31788,7 +33688,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mb_ereg_match` through `BuiltinLoweringContext`.", @@ -31925,7 +33825,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mb_strlen` through `BuiltinLoweringContext`.", @@ -32058,7 +33958,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.md5` through `BuiltinLoweringContext`.", @@ -32173,7 +34073,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.method_exists` through `BuiltinLoweringContext`.", @@ -32294,7 +34194,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.microtime` through `BuiltinLoweringContext`.", @@ -32395,7 +34295,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.min` through `BuiltinLoweringContext`.", @@ -32415,7 +34315,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -32423,7 +34325,7 @@ }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -32493,7 +34395,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mkdir` through `BuiltinLoweringContext`.", @@ -32637,7 +34539,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mktime` through `BuiltinLoweringContext`.", @@ -32792,7 +34694,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.mt_rand` through `BuiltinLoweringContext`.", @@ -32814,7 +34716,8 @@ "kind": "static", "names": [ "reads_process", - "writes_process" + "writes_process", + "may_throw" ] }, "lowering": { @@ -32899,7 +34802,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.natcasesort` through `BuiltinLoweringContext`.", @@ -33013,7 +34916,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.natsort` through `BuiltinLoweringContext`.", @@ -33095,6 +34998,118 @@ "slug": "natsort", "sub_area": "Array" }, + { + "area": "Array", + "canonical_name": "next", + "description": "Advances the array's internal pointer and returns the new element.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/next.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/array/next.rs", + "sig_line": null + }, + "name": "next", + "semantics": { + "argument_lowering": "array_internal_pointer", + "callable": { + "kind": "static_only", + "reason": "the internal array pointer needs a named array variable receiver" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "array_ptr_seek" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "next", + "sub_area": "Array" + }, { "area": "String", "canonical_name": "nl2br", @@ -33134,7 +35149,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.nl_to_br` through `BuiltinLoweringContext`.", @@ -33246,7 +35261,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.number_format` through `BuiltinLoweringContext`.", @@ -33357,7 +35372,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_clean` through `BuiltinLoweringContext`.", @@ -33456,7 +35471,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_end_clean` through `BuiltinLoweringContext`.", @@ -33555,7 +35570,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_end_flush` through `BuiltinLoweringContext`.", @@ -33654,7 +35669,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ob_flush` through `BuiltinLoweringContext`.", @@ -33695,19 +35710,218 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_flush" + "target": "ob_flush" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "ob_flush" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [], + "return_type": "bool", + "variadic": null + }, + "slug": "ob_flush", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "ob_get_clean", + "description": "Gets the current buffer contents and deletes the current output buffer.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_clean.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ob_get_clean` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/io/ob_get_clean.rs", + "sig_line": null + }, + "name": "ob_get_clean", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ob_get_clean" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ob_get_clean" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [], + "return_type": "mixed", + "variadic": null + }, + "slug": "ob_get_clean", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "ob_get_contents", + "description": "Returns the contents of the output buffer.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ob_get_contents` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/io/ob_get_contents.rs", + "sig_line": null + }, + "name": "ob_get_contents", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ob_get_contents" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ - "ob_flush" + "ob_get_contents" ], "target_strategy": "runtime_call", "target_support": [ @@ -33716,24 +35930,25 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": false } }, "sig": { "params": [], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ob_flush", + "slug": "ob_get_contents", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_get_clean", - "description": "Gets the current buffer contents and deletes the current output buffer.", + "canonical_name": "ob_get_flush", + "description": "Flushes the output buffer, returns it as a string and turns off output buffering.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_clean.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_flush.rs", "hooks": [ "direct", "values" @@ -33753,18 +35968,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_clean` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_get_flush` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_clean.rs", + "sig_file": "src/builtins/io/ob_get_flush.rs", "sig_line": null }, - "name": "ob_get_clean", + "name": "ob_get_flush", "semantics": { "argument_lowering": "standard", "callable": { @@ -33794,7 +36009,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_get_clean" + "target": "ob_get_flush" }, "ownership": { "argument_indexes": [], @@ -33806,7 +36021,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ob_get_clean" + "ob_get_flush" ], "target_strategy": "runtime_call", "target_support": [ @@ -33824,16 +36039,16 @@ "return_type": "mixed", "variadic": null }, - "slug": "ob_get_clean", + "slug": "ob_get_flush", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_get_contents", - "description": "Returns the contents of the output buffer.", + "canonical_name": "ob_get_length", + "description": "Returns the length of the output buffer.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_contents.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_length.rs", "hooks": [ "direct", "values" @@ -33853,18 +36068,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_contents` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_get_length` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_contents.rs", + "sig_file": "src/builtins/io/ob_get_length.rs", "sig_line": null }, - "name": "ob_get_contents", + "name": "ob_get_length", "semantics": { "argument_lowering": "standard", "callable": { @@ -33894,7 +36109,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_get_contents" + "target": "ob_get_length" }, "ownership": { "argument_indexes": [], @@ -33906,7 +36121,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ob_get_contents" + "ob_get_length" ], "target_strategy": "runtime_call", "target_support": [ @@ -33924,16 +36139,16 @@ "return_type": "mixed", "variadic": null }, - "slug": "ob_get_contents", + "slug": "ob_get_length", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_get_flush", - "description": "Flushes the output buffer, returns it as a string and turns off output buffering.", + "canonical_name": "ob_get_level", + "description": "Returns the nesting level of the output buffering mechanism.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_flush.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_level.rs", "hooks": [ "direct", "values" @@ -33953,18 +36168,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_flush` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_get_level` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_flush.rs", + "sig_file": "src/builtins/io/ob_get_level.rs", "sig_line": null }, - "name": "ob_get_flush", + "name": "ob_get_level", "semantics": { "argument_lowering": "standard", "callable": { @@ -33974,39 +36189,24 @@ "effects": { "kind": "static", "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" + "reads_global" ] }, "lowering": { "kind": "runtime_call", - "target": "ob_get_flush" + "target": "ob_get_level" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "declared", "runtime_functions": [ - "ob_get_flush" + "ob_get_level" ], "target_strategy": "runtime_call", "target_support": [ @@ -34015,31 +36215,37 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { "params": [], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "ob_get_flush", + "slug": "ob_get_level", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_get_length", - "description": "Returns the length of the output buffer.", + "canonical_name": "ob_get_status", + "description": "Gets status of output buffers.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_length.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_status.rs", "hooks": [ "direct", "values" ], "kind": "registry", - "params": [], + "params": [ + { + "by_ref": false, + "default": "false", + "name": "full_status", + "optional": true + } + ], "required_param_count": 0, "supported": true, "variadic": null @@ -34053,18 +36259,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_length` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_get_status` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_length.rs", + "sig_file": "src/builtins/io/ob_get_status.rs", "sig_line": null }, - "name": "ob_get_length", + "name": "ob_get_status", "semantics": { "argument_lowering": "standard", "callable": { @@ -34094,7 +36300,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_get_length" + "target": "ob_get_status" }, "ownership": { "argument_indexes": [], @@ -34104,9 +36310,9 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "shared", "runtime_functions": [ - "ob_get_length" + "ob_get_status" ], "target_strategy": "runtime_call", "target_support": [ @@ -34120,104 +36326,28 @@ } }, "sig": { - "params": [], - "return_type": "mixed", - "variadic": null - }, - "slug": "ob_get_length", - "sub_area": "IO" - }, - { - "area": "IO", - "canonical_name": "ob_get_level", - "description": "Returns the nesting level of the output buffering mechanism.", - "eval": { - "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_level.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [], - "required_param_count": 0, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_level` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_level.rs", - "sig_line": null - }, - "name": "ob_get_level", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_global" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "ob_get_level" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "declared", - "runtime_functions": [ - "ob_get_level" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" + "params": [ + { + "by_ref": false, + "default": "false", + "name": "full_status", + "optional": true, + "type": "bool" + } ], - "validation": { - "kind": "signature" - } - }, - "sig": { - "params": [], - "return_type": "int", + "return_type": "array", "variadic": null }, - "slug": "ob_get_level", + "slug": "ob_get_status", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_get_status", - "description": "Gets status of output buffers.", + "canonical_name": "ob_implicit_flush", + "description": "Turns implicit flush on/off.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_get_status.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_implicit_flush.rs", "hooks": [ "direct", "values" @@ -34226,8 +36356,8 @@ "params": [ { "by_ref": false, - "default": "false", - "name": "full_status", + "default": "true", + "name": "enable", "optional": true } ], @@ -34244,18 +36374,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_get_status` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_implicit_flush` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_get_status.rs", + "sig_file": "src/builtins/io/ob_implicit_flush.rs", "sig_line": null }, - "name": "ob_get_status", + "name": "ob_implicit_flush", "semantics": { "argument_lowering": "standard", "callable": { @@ -34285,19 +36415,19 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_get_status" + "target": "ob_implicit_flush" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "shared", + "result_type": "declared", "runtime_functions": [ - "ob_get_status" + "ob_implicit_flush" ], "target_strategy": "runtime_call", "target_support": [ @@ -34306,46 +36436,38 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { "params": [ { "by_ref": false, - "default": "false", - "name": "full_status", + "default": "true", + "name": "enable", "optional": true, "type": "bool" } ], - "return_type": "array", + "return_type": "bool", "variadic": null }, - "slug": "ob_get_status", + "slug": "ob_implicit_flush", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_implicit_flush", - "description": "Turns implicit flush on/off.", + "canonical_name": "ob_list_handlers", + "description": "Lists all output handlers in use.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_implicit_flush.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_list_handlers.rs", "hooks": [ "direct", "values" ], "kind": "registry", - "params": [ - { - "by_ref": false, - "default": "true", - "name": "enable", - "optional": true - } - ], + "params": [], "required_param_count": 0, "supported": true, "variadic": null @@ -34359,18 +36481,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_implicit_flush` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_list_handlers` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_implicit_flush.rs", + "sig_file": "src/builtins/io/ob_list_handlers.rs", "sig_line": null }, - "name": "ob_implicit_flush", + "name": "ob_list_handlers", "semantics": { "argument_lowering": "standard", "callable": { @@ -34400,19 +36522,19 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_implicit_flush" + "target": "ob_list_handlers" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ - "ob_implicit_flush" + "ob_list_handlers" ], "target_strategy": "runtime_call", "target_support": [ @@ -34421,38 +36543,50 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": false } }, "sig": { - "params": [ - { - "by_ref": false, - "default": "true", - "name": "enable", - "optional": true, - "type": "bool" - } - ], - "return_type": "bool", + "params": [], + "return_type": "array", "variadic": null }, - "slug": "ob_implicit_flush", + "slug": "ob_list_handlers", "sub_area": "IO" }, { "area": "IO", - "canonical_name": "ob_list_handlers", - "description": "Lists all output handlers in use.", + "canonical_name": "ob_start", + "description": "Turns on output buffering.", "eval": { "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_list_handlers.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_start.rs", "hooks": [ "direct", "values" ], "kind": "registry", - "params": [], + "params": [ + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "chunk_size", + "optional": true + }, + { + "by_ref": false, + "default": "112", + "name": "flags", + "optional": true + } + ], "required_param_count": 0, "supported": true, "variadic": null @@ -34466,18 +36600,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_list_handlers` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ob_start` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_list_handlers.rs", + "sig_file": "src/builtins/io/ob_start.rs", "sig_line": null }, - "name": "ob_list_handlers", + "name": "ob_start", "semantics": { "argument_lowering": "standard", "callable": { @@ -34507,11 +36641,11 @@ }, "lowering": { "kind": "runtime_call", - "target": "ob_list_handlers" + "target": "ob_start" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", @@ -34519,7 +36653,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ob_list_handlers" + "ob_start" ], "target_strategy": "runtime_call", "target_support": [ @@ -34533,49 +36667,43 @@ } }, "sig": { - "params": [], - "return_type": "array", - "variadic": null - }, - "slug": "ob_list_handlers", - "sub_area": "IO" - }, - { - "area": "IO", - "canonical_name": "ob_start", - "description": "Turns on output buffering.", - "eval": { - "area": "core", - "home_file": "crates/elephc-magician/src/interpreter/builtins/core/ob_start.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", "params": [ { "by_ref": false, "default": "null", "name": "callback", - "optional": true + "optional": true, + "type": "mixed" }, { "by_ref": false, "default": "0", "name": "chunk_size", - "optional": true + "optional": true, + "type": "int" }, { "by_ref": false, "default": "112", "name": "flags", - "optional": true + "optional": true, + "type": "int" } ], - "required_param_count": 0, - "supported": true, + "return_type": "bool", "variadic": null }, + "slug": "ob_start", + "sub_area": "IO" + }, + { + "area": "Math", + "canonical_name": "octdec", + "description": "Converts a octal string to its decimal number.", + "eval": { + "kind": "none", + "supported": false + }, "eval_only": false, "in_catalog": true, "is_extension": false, @@ -34585,18 +36713,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ob_start` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.octdec` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/io/ob_start.rs", + "sig_file": "src/builtins/math/octdec.rs", "sig_line": null }, - "name": "ob_start", + "name": "octdec", "semantics": { "argument_lowering": "standard", "callable": { @@ -34605,32 +36733,15 @@ }, "effects": { "kind": "static", - "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" - ] + "names": [] }, "lowering": { "kind": "runtime_call", - "target": "ob_start" + "target": "octdec" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -34638,7 +36749,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ob_start" + "octdec" ], "target_strategy": "runtime_call", "target_support": [ @@ -34655,31 +36766,17 @@ "params": [ { "by_ref": false, - "default": "null", - "name": "callback", - "optional": true, - "type": "mixed" - }, - { - "by_ref": false, - "default": "0", - "name": "chunk_size", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "112", - "name": "flags", - "optional": true, - "type": "int" + "default": null, + "name": "octal_string", + "optional": false, + "type": "string" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ob_start", - "sub_area": "IO" + "slug": "octdec", + "sub_area": "Math" }, { "area": "IO", @@ -34714,7 +36811,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.opendir` through `BuiltinLoweringContext`.", @@ -34829,7 +36926,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ord` through `BuiltinLoweringContext`.", @@ -34932,7 +37029,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.parse_url` through `BuiltinLoweringContext`.", @@ -35039,7 +37136,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.passthru` through `BuiltinLoweringContext`.", @@ -35159,7 +37256,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pathinfo` through `BuiltinLoweringContext`.", @@ -35281,7 +37378,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pclose` through `BuiltinLoweringContext`.", @@ -35419,7 +37516,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pfsockopen` through `BuiltinLoweringContext`.", @@ -35562,7 +37659,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.php_uname` through `BuiltinLoweringContext`.", @@ -35664,7 +37761,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.phpversion` through `BuiltinLoweringContext`.", @@ -35755,7 +37852,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pi` through `BuiltinLoweringContext`.", @@ -35850,7 +37947,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.popen` through `BuiltinLoweringContext`.", @@ -35978,7 +38075,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.pow` through `BuiltinLoweringContext`.", @@ -36100,7 +38197,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_match` through `BuiltinLoweringContext`.", @@ -36247,7 +38344,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_match_all` through `BuiltinLoweringContext`.", @@ -36380,7 +38477,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.preg_replace` through `BuiltinLoweringContext`.", @@ -36421,7 +38518,147 @@ }, "lowering": { "kind": "runtime_call", - "target": "preg_replace" + "target": "preg_replace" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "preg_replace" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "replacement", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "preg_replace", + "sub_area": "Regex" + }, + { + "area": "Regex", + "canonical_name": "preg_replace_callback", + "description": "Performs a regular expression search and replace using a callback.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.preg_replace_callback` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/preg_replace_callback.rs", + "sig_line": null + }, + "name": "preg_replace_callback", + "semantics": { + "argument_lowering": "preg_replace_callback", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "preg_replace_callback" }, "ownership": { "argument_indexes": [], @@ -36431,9 +38668,9 @@ "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ - "preg_replace" + "preg_replace_callback" ], "target_strategy": "runtime_call", "target_support": [ @@ -36442,7 +38679,8 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": true } }, "sig": { @@ -36457,9 +38695,9 @@ { "by_ref": false, "default": null, - "name": "replacement", + "name": "callback", "optional": false, - "type": "string" + "type": "callable" }, { "by_ref": false, @@ -36472,16 +38710,16 @@ "return_type": "string", "variadic": null }, - "slug": "preg_replace", + "slug": "preg_replace_callback", "sub_area": "Regex" }, { "area": "Regex", - "canonical_name": "preg_replace_callback", - "description": "Performs a regular expression search and replace using a callback.", + "canonical_name": "preg_split", + "description": "Splits a string by a regular expression.", "eval": { "area": "regex", - "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs", "hooks": [ "direct", "values" @@ -36497,17 +38735,23 @@ { "by_ref": false, "default": null, - "name": "callback", + "name": "subject", "optional": false }, { "by_ref": false, - "default": null, - "name": "subject", - "optional": false + "default": "-1", + "name": "limit", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true } ], - "required_param_count": 3, + "required_param_count": 2, "supported": true, "variadic": null }, @@ -36520,20 +38764,20 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.preg_replace_callback` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.preg_split` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/preg_replace_callback.rs", + "sig_file": "src/builtins/system/preg_split.rs", "sig_line": null }, - "name": "preg_replace_callback", + "name": "preg_split", "semantics": { - "argument_lowering": "preg_replace_callback", + "argument_lowering": "positional_regex", "callable": { "kind": "static_only", "reason": "typed backend operation has no runtime-selected wrapper contract" @@ -36561,19 +38805,19 @@ }, "lowering": { "kind": "runtime_call", - "target": "preg_replace_callback" + "target": "preg_split" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "shared", "runtime_functions": [ - "preg_replace_callback" + "preg_split" ], "target_strategy": "runtime_call", "target_support": [ @@ -36583,7 +38827,7 @@ ], "validation": { "kind": "checker_hook", - "lazy": true + "lazy": false } }, "sig": { @@ -36598,63 +38842,51 @@ { "by_ref": false, "default": null, - "name": "callback", + "name": "subject", "optional": false, - "type": "callable" + "type": "string" }, { "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "string" + "default": "-1", + "name": "limit", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true, + "type": "int" } ], - "return_type": "string", + "return_type": "array", "variadic": null }, - "slug": "preg_replace_callback", + "slug": "preg_split", "sub_area": "Regex" }, { - "area": "Regex", - "canonical_name": "preg_split", - "description": "Splits a string by a regular expression.", + "area": "Array", + "canonical_name": "prev", + "description": "Rewinds the array's internal pointer and returns the new element.", "eval": { - "area": "regex", - "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs", + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/prev.rs", "hooks": [ - "direct", "values" ], "kind": "registry", "params": [ { - "by_ref": false, - "default": null, - "name": "pattern", - "optional": false - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "subject", + "name": "array", "optional": false - }, - { - "by_ref": false, - "default": "-1", - "name": "limit", - "optional": true - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, @@ -36667,23 +38899,23 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.preg_split` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/preg_split.rs", + "sig_file": "src/builtins/array/prev.rs", "sig_line": null }, - "name": "preg_split", + "name": "prev", "semantics": { - "argument_lowering": "positional_regex", + "argument_lowering": "array_internal_pointer", "callable": { "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" + "reason": "the internal array pointer needs a named array variable receiver" }, "effects": { "kind": "static", @@ -36708,7 +38940,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "preg_split" + "target": "array_ptr_seek" }, "ownership": { "argument_indexes": [], @@ -36718,10 +38950,8 @@ "kind": "static", "values": [] }, - "result_type": "shared", - "runtime_functions": [ - "preg_split" - ], + "result_type": "checked", + "runtime_functions": [], "target_strategy": "runtime_call", "target_support": [ "macos-aarch64", @@ -36736,39 +38966,18 @@ "sig": { "params": [ { - "by_ref": false, - "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, + "by_ref": true, "default": null, - "name": "subject", + "name": "array", "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": "-1", - "name": "limit", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "0", - "name": "flags", - "optional": true, - "type": "int" + "type": "array" } ], - "return_type": "array", + "return_type": "mixed", "variadic": null }, - "slug": "preg_split", - "sub_area": "Regex" + "slug": "prev", + "sub_area": "Array" }, { "area": "Misc", @@ -36809,7 +39018,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.print_r` through `BuiltinLoweringContext`.", @@ -36931,7 +39140,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.printf` through `BuiltinLoweringContext`.", @@ -37051,7 +39260,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.property_exists` through `BuiltinLoweringContext`.", @@ -37092,7 +39301,128 @@ }, "lowering": { "kind": "runtime_call", - "target": "property_exists" + "target": "property_exists" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "property_exists" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + }, + { + "by_ref": false, + "default": null, + "name": "property", + "optional": false, + "type": "string" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "property_exists", + "sub_area": "Class" + }, + { + "area": "Pointer", + "canonical_name": "ptr", + "description": "Returns a raw pointer to the given variable.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr.rs", + "sig_line": null + }, + "name": "ptr", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr" }, "ownership": { "argument_indexes": [], @@ -37102,9 +39432,9 @@ "kind": "static", "values": [] }, - "result_type": "declared", + "result_type": "checked", "runtime_functions": [ - "property_exists" + "ptr" ], "target_strategy": "runtime_call", "target_support": [ @@ -37113,7 +39443,8 @@ "linux-x86_64" ], "validation": { - "kind": "signature" + "kind": "checker_hook", + "lazy": false } }, "sig": { @@ -37121,31 +39452,24 @@ { "by_ref": false, "default": null, - "name": "object_or_class", + "name": "value", "optional": false, "type": "mixed" - }, - { - "by_ref": false, - "default": null, - "name": "property", - "optional": false, - "type": "string" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "property_exists", - "sub_area": "Class" + "slug": "ptr", + "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr", - "description": "Returns a raw pointer to the given variable.", + "canonical_name": "ptr_get", + "description": "Reads one machine word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", "hooks": [ "direct", "values" @@ -37155,7 +39479,7 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "pointer", "optional": false } ], @@ -37172,18 +39496,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_get` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr.rs", + "sig_file": "src/builtins/pointers/ptr_get.rs", "sig_line": null }, - "name": "ptr", + "name": "ptr_get", "semantics": { "argument_lowering": "standard", "callable": { @@ -37213,7 +39537,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr" + "target": "ptr_get" }, "ownership": { "argument_indexes": [], @@ -37225,7 +39549,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr" + "ptr_get" ], "target_strategy": "runtime_call", "target_support": [ @@ -37243,24 +39567,24 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "pointer", "optional": false, - "type": "mixed" + "type": "pointer" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "ptr", + "slug": "ptr_get", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_get", - "description": "Reads one machine word through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_is_null", + "description": "Returns true if the pointer is null.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", "hooks": [ "direct", "values" @@ -37287,18 +39611,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_get` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_is_null` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_get.rs", + "sig_file": "src/builtins/pointers/ptr_is_null.rs", "sig_line": null }, - "name": "ptr_get", + "name": "ptr_is_null", "semantics": { "argument_lowering": "standard", "callable": { @@ -37328,7 +39652,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_get" + "target": "ptr_is_null" }, "ownership": { "argument_indexes": [], @@ -37340,7 +39664,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_get" + "ptr_is_null" ], "target_strategy": "runtime_call", "target_support": [ @@ -37363,19 +39687,119 @@ "type": "pointer" } ], - "return_type": "int", + "return_type": "bool", "variadic": null }, - "slug": "ptr_get", + "slug": "ptr_is_null", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_is_null", - "description": "Returns true if the pointer is null.", + "canonical_name": "ptr_null", + "description": "Returns a null raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.ptr_null` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/pointers/ptr_null.rs", + "sig_line": null + }, + "name": "ptr_null", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "ptr_null" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "ptr_null" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [], + "return_type": "mixed", + "variadic": null + }, + "slug": "ptr_null", + "sub_area": "Pointer" + }, + { + "area": "Pointer", + "canonical_name": "ptr_offset", + "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", "hooks": [ "direct", "values" @@ -37387,9 +39811,15 @@ "default": null, "name": "pointer", "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false } ], - "required_param_count": 1, + "required_param_count": 2, "supported": true, "variadic": null }, @@ -37402,18 +39832,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_is_null` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_offset` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_is_null.rs", + "sig_file": "src/builtins/pointers/ptr_offset.rs", "sig_line": null }, - "name": "ptr_is_null", + "name": "ptr_offset", "semantics": { "argument_lowering": "standard", "callable": { @@ -37443,7 +39873,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_is_null" + "target": "ptr_offset" }, "ownership": { "argument_indexes": [], @@ -37455,7 +39885,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_is_null" + "ptr_offset" ], "target_strategy": "runtime_call", "target_support": [ @@ -37476,28 +39906,42 @@ "name": "pointer", "optional": false, "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false, + "type": "int" } ], - "return_type": "bool", + "return_type": "mixed", "variadic": null }, - "slug": "ptr_is_null", + "slug": "ptr_offset", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_null", - "description": "Returns a null raw pointer.", + "canonical_name": "ptr_read16", + "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", "hooks": [ "direct", "values" ], "kind": "registry", - "params": [], - "required_param_count": 0, + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, "supported": true, "variadic": null }, @@ -37510,18 +39954,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_null` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read16` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_null.rs", + "sig_file": "src/builtins/pointers/ptr_read16.rs", "sig_line": null }, - "name": "ptr_null", + "name": "ptr_read16", "semantics": { "argument_lowering": "standard", "callable": { @@ -37551,7 +39995,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_null" + "target": "ptr_read16" }, "ownership": { "argument_indexes": [], @@ -37563,7 +40007,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_null" + "ptr_read16" ], "target_strategy": "runtime_call", "target_support": [ @@ -37577,20 +40021,28 @@ } }, "sig": { - "params": [], - "return_type": "mixed", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false, + "type": "pointer" + } + ], + "return_type": "int", "variadic": null }, - "slug": "ptr_null", + "slug": "ptr_read16", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_offset", - "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "canonical_name": "ptr_read32", + "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", "hooks": [ "direct", "values" @@ -37602,15 +40054,9 @@ "default": null, "name": "pointer", "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, @@ -37623,18 +40069,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_offset` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read32` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_offset.rs", + "sig_file": "src/builtins/pointers/ptr_read32.rs", "sig_line": null }, - "name": "ptr_offset", + "name": "ptr_read32", "semantics": { "argument_lowering": "standard", "callable": { @@ -37664,7 +40110,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_offset" + "target": "ptr_read32" }, "ownership": { "argument_indexes": [], @@ -37676,7 +40122,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_offset" + "ptr_read32" ], "target_strategy": "runtime_call", "target_support": [ @@ -37697,28 +40143,21 @@ "name": "pointer", "optional": false, "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "offset", - "optional": false, - "type": "int" } ], - "return_type": "mixed", + "return_type": "int", "variadic": null }, - "slug": "ptr_offset", + "slug": "ptr_read32", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read16", - "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_read8", + "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", "hooks": [ "direct", "values" @@ -37745,18 +40184,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read16` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read8` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read16.rs", + "sig_file": "src/builtins/pointers/ptr_read8.rs", "sig_line": null }, - "name": "ptr_read16", + "name": "ptr_read8", "semantics": { "argument_lowering": "standard", "callable": { @@ -37786,7 +40225,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read16" + "target": "ptr_read8" }, "ownership": { "argument_indexes": [], @@ -37798,7 +40237,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read16" + "ptr_read8" ], "target_strategy": "runtime_call", "target_support": [ @@ -37824,16 +40263,16 @@ "return_type": "int", "variadic": null }, - "slug": "ptr_read16", + "slug": "ptr_read8", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read32", - "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", + "canonical_name": "ptr_read_string", + "description": "Copies raw bytes from a pointer into a PHP string of the given length.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", "hooks": [ "direct", "values" @@ -37845,124 +40284,15 @@ "default": null, "name": "pointer", "optional": false - } - ], - "required_param_count": 1, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read32` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read32.rs", - "sig_line": null - }, - "name": "ptr_read32", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" - }, - "effects": { - "kind": "static", - "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" - ] - }, - "lowering": { - "kind": "runtime_call", - "target": "ptr_read32" - }, - "ownership": { - "argument_indexes": [], - "kind": "may_alias_arguments" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "checked", - "runtime_functions": [ - "ptr_read32" - ], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "checker_hook", - "lazy": false - } - }, - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - } - ], - "return_type": "int", - "variadic": null - }, - "slug": "ptr_read32", - "sub_area": "Pointer" - }, - { - "area": "Pointer", - "canonical_name": "ptr_read8", - "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", - "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [ + }, { "by_ref": false, "default": null, - "name": "pointer", + "name": "length", "optional": false } ], - "required_param_count": 1, + "required_param_count": 2, "supported": true, "variadic": null }, @@ -37975,18 +40305,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read8` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_read_string` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read8.rs", + "sig_file": "src/builtins/pointers/ptr_read_string.rs", "sig_line": null }, - "name": "ptr_read8", + "name": "ptr_read_string", "semantics": { "argument_lowering": "standard", "callable": { @@ -38016,11 +40346,11 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read8" + "target": "ptr_read_string" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", @@ -38028,7 +40358,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read8" + "ptr_read_string" ], "target_strategy": "runtime_call", "target_support": [ @@ -38049,21 +40379,28 @@ "name": "pointer", "optional": false, "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "ptr_read8", + "slug": "ptr_read_string", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_read_string", - "description": "Copies raw bytes from a pointer into a PHP string of the given length.", + "canonical_name": "ptr_set", + "description": "Writes one machine word through a raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", "hooks": [ "direct", "values" @@ -38079,7 +40416,7 @@ { "by_ref": false, "default": null, - "name": "length", + "name": "value", "optional": false } ], @@ -38096,18 +40433,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_read_string` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_set` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_read_string.rs", + "sig_file": "src/builtins/pointers/ptr_set.rs", "sig_line": null }, - "name": "ptr_read_string", + "name": "ptr_set", "semantics": { "argument_lowering": "standard", "callable": { @@ -38137,11 +40474,11 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_read_string" + "target": "ptr_set" }, "ownership": { "argument_indexes": [], - "kind": "fresh" + "kind": "may_alias_arguments" }, "requirements": { "kind": "static", @@ -38149,7 +40486,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_read_string" + "ptr_set" ], "target_strategy": "runtime_call", "target_support": [ @@ -38174,24 +40511,24 @@ { "by_ref": false, "default": null, - "name": "length", + "name": "value", "optional": false, - "type": "int" + "type": "mixed" } ], - "return_type": "string", + "return_type": "void", "variadic": null }, - "slug": "ptr_read_string", + "slug": "ptr_set", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_set", - "description": "Writes one machine word through a raw pointer.", + "canonical_name": "ptr_sizeof", + "description": "Returns the byte size of the named pointer target type.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", "hooks": [ "direct", "values" @@ -38201,17 +40538,11 @@ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "value", + "name": "type", "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, @@ -38224,18 +40555,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_set` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_sizeof` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_set.rs", + "sig_file": "src/builtins/pointers/ptr_sizeof.rs", "sig_line": null }, - "name": "ptr_set", + "name": "ptr_sizeof", "semantics": { "argument_lowering": "standard", "callable": { @@ -38265,7 +40596,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_set" + "target": "ptr_sizeof" }, "ownership": { "argument_indexes": [], @@ -38277,7 +40608,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_set" + "ptr_sizeof" ], "target_strategy": "runtime_call", "target_support": [ @@ -38295,31 +40626,24 @@ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false, - "type": "pointer" - }, - { - "by_ref": false, - "default": null, - "name": "value", + "name": "type", "optional": false, - "type": "mixed" + "type": "string" } ], - "return_type": "void", + "return_type": "int", "variadic": null }, - "slug": "ptr_set", + "slug": "ptr_sizeof", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_sizeof", - "description": "Returns the byte size of the named pointer target type.", + "canonical_name": "ptr_write16", + "description": "Writes one 16-bit word through a raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", "hooks": [ "direct", "values" @@ -38329,11 +40653,17 @@ { "by_ref": false, "default": null, - "name": "type", + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", "optional": false } ], - "required_param_count": 1, + "required_param_count": 2, "supported": true, "variadic": null }, @@ -38346,18 +40676,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_sizeof` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_write16` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_sizeof.rs", + "sig_file": "src/builtins/pointers/ptr_write16.rs", "sig_line": null }, - "name": "ptr_sizeof", + "name": "ptr_write16", "semantics": { "argument_lowering": "standard", "callable": { @@ -38387,7 +40717,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_sizeof" + "target": "ptr_write16" }, "ownership": { "argument_indexes": [], @@ -38399,7 +40729,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_sizeof" + "ptr_write16" ], "target_strategy": "runtime_call", "target_support": [ @@ -38417,24 +40747,31 @@ { "by_ref": false, "default": null, - "name": "type", + "name": "pointer", "optional": false, - "type": "string" + "type": "pointer" + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false, + "type": "int" } ], - "return_type": "int", + "return_type": "void", "variadic": null }, - "slug": "ptr_sizeof", + "slug": "ptr_write16", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write16", - "description": "Writes one 16-bit word through a raw pointer.", + "canonical_name": "ptr_write32", + "description": "Writes one 32-bit word through a raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs", "hooks": [ "direct", "values" @@ -38467,18 +40804,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_write16` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_write32` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_write16.rs", + "sig_file": "src/builtins/pointers/ptr_write32.rs", "sig_line": null }, - "name": "ptr_write16", + "name": "ptr_write32", "semantics": { "argument_lowering": "standard", "callable": { @@ -38508,7 +40845,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_write16" + "target": "ptr_write32" }, "ownership": { "argument_indexes": [], @@ -38520,7 +40857,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_write16" + "ptr_write32" ], "target_strategy": "runtime_call", "target_support": [ @@ -38553,16 +40890,16 @@ "return_type": "void", "variadic": null }, - "slug": "ptr_write16", + "slug": "ptr_write32", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write32", - "description": "Writes one 32-bit word through a raw pointer.", + "canonical_name": "ptr_write8", + "description": "Writes one byte through a raw pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs", "hooks": [ "direct", "values" @@ -38595,18 +40932,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_write32` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_write8` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_write32.rs", + "sig_file": "src/builtins/pointers/ptr_write8.rs", "sig_line": null }, - "name": "ptr_write32", + "name": "ptr_write8", "semantics": { "argument_lowering": "standard", "callable": { @@ -38636,7 +40973,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_write32" + "target": "ptr_write8" }, "ownership": { "argument_indexes": [], @@ -38648,7 +40985,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_write32" + "ptr_write8" ], "target_strategy": "runtime_call", "target_support": [ @@ -38681,16 +41018,16 @@ "return_type": "void", "variadic": null }, - "slug": "ptr_write32", + "slug": "ptr_write8", "sub_area": "Pointer" }, { "area": "Pointer", - "canonical_name": "ptr_write8", - "description": "Writes one byte through a raw pointer.", + "canonical_name": "ptr_write_string", + "description": "Copies PHP string bytes into raw memory at the given pointer.", "eval": { "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs", "hooks": [ "direct", "values" @@ -38706,7 +41043,7 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "string", "optional": false } ], @@ -38723,18 +41060,18 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_write8` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.ptr_write_string` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_write8.rs", + "sig_file": "src/builtins/pointers/ptr_write_string.rs", "sig_line": null }, - "name": "ptr_write8", + "name": "ptr_write_string", "semantics": { "argument_lowering": "standard", "callable": { @@ -38764,7 +41101,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_write8" + "target": "ptr_write_string" }, "ownership": { "argument_indexes": [], @@ -38776,7 +41113,7 @@ }, "result_type": "checked", "runtime_functions": [ - "ptr_write8" + "ptr_write_string" ], "target_strategy": "runtime_call", "target_support": [ @@ -38801,24 +41138,24 @@ { "by_ref": false, "default": null, - "name": "value", + "name": "string", "optional": false, - "type": "int" + "type": "string" } ], - "return_type": "void", + "return_type": "int", "variadic": null }, - "slug": "ptr_write8", + "slug": "ptr_write_string", "sub_area": "Pointer" }, { - "area": "Pointer", - "canonical_name": "ptr_write_string", - "description": "Copies PHP string bytes into raw memory at the given pointer.", + "area": "Filesystem", + "canonical_name": "putenv", + "description": "Sets an environment variable.", "eval": { - "area": "raw_memory", - "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs", + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs", "hooks": [ "direct", "values" @@ -38828,41 +41165,35 @@ { "by_ref": false, "default": null, - "name": "pointer", - "optional": false - }, - { - "by_ref": false, - "default": null, - "name": "string", + "name": "assignment", "optional": false } ], - "required_param_count": 2, + "required_param_count": 1, "supported": true, "variadic": null }, "eval_only": false, "in_catalog": true, - "is_extension": true, + "is_extension": false, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.ptr_write_string` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.putenv` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/pointers/ptr_write_string.rs", + "sig_file": "src/builtins/system/putenv.rs", "sig_line": null }, - "name": "ptr_write_string", + "name": "putenv", "semantics": { "argument_lowering": "standard", "callable": { @@ -38892,7 +41223,7 @@ }, "lowering": { "kind": "runtime_call", - "target": "ptr_write_string" + "target": "putenv" }, "ownership": { "argument_indexes": [], @@ -38902,9 +41233,9 @@ "kind": "static", "values": [] }, - "result_type": "checked", + "result_type": "declared", "runtime_functions": [ - "ptr_write_string" + "putenv" ], "target_strategy": "runtime_call", "target_support": [ @@ -38913,8 +41244,7 @@ "linux-x86_64" ], "validation": { - "kind": "checker_hook", - "lazy": false + "kind": "signature" } }, "sig": { @@ -38922,10 +41252,97 @@ { "by_ref": false, "default": null, - "name": "pointer", + "name": "assignment", "optional": false, - "type": "pointer" - }, + "type": "string" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "putenv", + "sub_area": "Filesystem" + }, + { + "area": "String", + "canonical_name": "quoted_printable_encode", + "description": "Encodes a string with the MIME quoted-printable transfer encoding.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/quoted_printable_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.string.quoted_printable_encode` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/quoted_printable_encode.rs", + "sig_line": null + }, + "name": "quoted_printable_encode", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "dynamic" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "string.quoted_printable_encode" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ { "by_ref": false, "default": null, @@ -38934,19 +41351,19 @@ "type": "string" } ], - "return_type": "int", + "return_type": "string", "variadic": null }, - "slug": "ptr_write_string", - "sub_area": "Pointer" + "slug": "quoted_printable_encode", + "sub_area": "String" }, { - "area": "Filesystem", - "canonical_name": "putenv", - "description": "Sets an environment variable.", + "area": "String", + "canonical_name": "quotemeta", + "description": "Prefixes each regular-expression metacharacter in a string with a backslash.", "eval": { - "area": "network_env", - "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs", + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/quotemeta.rs", "hooks": [ "direct", "values" @@ -38956,7 +41373,7 @@ { "by_ref": false, "default": null, - "name": "assignment", + "name": "string", "optional": false } ], @@ -38973,61 +41390,41 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.putenv` through `BuiltinLoweringContext`.", + "Emits the typed EIR target `runtime.string.quote_meta` through `BuiltinLoweringContext`.", "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." ], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/system/putenv.rs", + "sig_file": "src/builtins/string/quotemeta.rs", "sig_line": null }, - "name": "putenv", + "name": "quotemeta", "semantics": { "argument_lowering": "standard", "callable": { - "kind": "static_only", - "reason": "typed backend operation has no runtime-selected wrapper contract" + "kind": "dynamic" }, "effects": { "kind": "static", - "names": [ - "reads_local", - "writes_local", - "reads_heap", - "writes_heap", - "reads_global", - "reads_fs", - "writes_fs", - "reads_process", - "writes_process", - "output", - "alloc_heap", - "alloc_concat", - "may_throw", - "may_fatal", - "may_warn", - "may_deopt" - ] + "names": [] }, "lowering": { "kind": "runtime_call", - "target": "putenv" + "target": "string.quote_meta" }, "ownership": { "argument_indexes": [], - "kind": "may_alias_arguments" + "kind": "fresh" }, "requirements": { "kind": "static", "values": [] }, "result_type": "declared", - "runtime_functions": [ - "putenv" - ], + "runtime_functions": [], "target_strategy": "runtime_call", "target_support": [ "macos-aarch64", @@ -39043,16 +41440,16 @@ { "by_ref": false, "default": null, - "name": "assignment", + "name": "string", "optional": false, "type": "string" } ], - "return_type": "bool", + "return_type": "string", "variadic": null }, - "slug": "putenv", - "sub_area": "Filesystem" + "slug": "quotemeta", + "sub_area": "String" }, { "area": "Math", @@ -39087,7 +41484,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rad2deg` through `BuiltinLoweringContext`.", @@ -39190,7 +41587,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rand` through `BuiltinLoweringContext`.", @@ -39304,7 +41701,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.random_int` through `BuiltinLoweringContext`.", @@ -39326,7 +41723,8 @@ "kind": "static", "names": [ "reads_process", - "writes_process" + "writes_process", + "may_throw" ] }, "lowering": { @@ -39402,6 +41800,12 @@ "default": null, "name": "end", "optional": false + }, + { + "by_ref": false, + "default": "1", + "name": "step", + "optional": true } ], "required_param_count": 2, @@ -39417,7 +41821,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.range` through `BuiltinLoweringContext`.", @@ -39437,7 +41841,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -39481,6 +41887,13 @@ "name": "end", "optional": false, "type": "mixed" + }, + { + "by_ref": false, + "default": "1", + "name": "step", + "optional": true, + "type": "int" } ], "return_type": "array", @@ -39522,7 +41935,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.raw_url_decode` through `BuiltinLoweringContext`.", @@ -39616,7 +42029,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.raw_url_encode` through `BuiltinLoweringContext`.", @@ -39710,7 +42123,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readdir` through `BuiltinLoweringContext`.", @@ -39825,7 +42238,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readfile` through `BuiltinLoweringContext`.", @@ -39940,7 +42353,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readline` through `BuiltinLoweringContext`.", @@ -40055,7 +42468,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.readlink` through `BuiltinLoweringContext`.", @@ -40170,7 +42583,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath` through `BuiltinLoweringContext`.", @@ -40278,7 +42691,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath_cache_get` through `BuiltinLoweringContext`.", @@ -40378,7 +42791,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.realpath_cache_size` through `BuiltinLoweringContext`.", @@ -40490,7 +42903,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rename` through `BuiltinLoweringContext`.", @@ -40578,6 +42991,118 @@ "slug": "rename", "sub_area": "Filesystem" }, + { + "area": "Array", + "canonical_name": "reset", + "description": "Rewinds the array's internal pointer to the first element and returns it.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/reset.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.array_ptr_seek` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/array/reset.rs", + "sig_line": null + }, + "name": "reset", + "semantics": { + "argument_lowering": "array_internal_pointer", + "callable": { + "kind": "static_only", + "reason": "the internal array pointer needs a named array variable receiver" + }, + "effects": { + "kind": "static", + "names": [ + "reads_local", + "writes_local", + "reads_heap", + "writes_heap", + "reads_global", + "reads_fs", + "writes_fs", + "reads_process", + "writes_process", + "output", + "alloc_heap", + "alloc_concat", + "may_throw", + "may_fatal", + "may_warn", + "may_deopt" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "array_ptr_seek" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false, + "type": "array" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "reset", + "sub_area": "Array" + }, { "area": "IO", "canonical_name": "rewind", @@ -40611,7 +43136,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rewind` through `BuiltinLoweringContext`.", @@ -40726,7 +43251,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rewinddir` through `BuiltinLoweringContext`.", @@ -40841,7 +43366,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rmdir` through `BuiltinLoweringContext`.", @@ -40946,6 +43471,12 @@ "default": "0", "name": "precision", "optional": true + }, + { + "by_ref": false, + "default": "1", + "name": "mode", + "optional": true } ], "required_param_count": 1, @@ -40961,7 +43492,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.round` through `BuiltinLoweringContext`.", @@ -40981,7 +43512,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -41024,6 +43557,13 @@ "name": "precision", "optional": true, "type": "int" + }, + { + "by_ref": false, + "default": "1", + "name": "mode", + "optional": true, + "type": "int" } ], "return_type": "float", @@ -41064,7 +43604,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rsort` through `BuiltinLoweringContext`.", @@ -41185,7 +43725,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.rtrim` through `BuiltinLoweringContext`.", @@ -41289,7 +43829,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.scandir` through `BuiltinLoweringContext`.", @@ -41388,7 +43928,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.serialize` through `BuiltinLoweringContext`.", @@ -41507,7 +44047,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.settype` through `BuiltinLoweringContext`.", @@ -41635,7 +44175,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sha1` through `BuiltinLoweringContext`.", @@ -41744,7 +44284,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.shell_exec` through `BuiltinLoweringContext`.", @@ -41857,7 +44397,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.shuffle` through `BuiltinLoweringContext`.", @@ -41972,7 +44512,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sin` through `BuiltinLoweringContext`.", @@ -42069,7 +44609,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sinh` through `BuiltinLoweringContext`.", @@ -42166,7 +44706,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sleep` through `BuiltinLoweringContext`.", @@ -42264,7 +44804,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sort` through `BuiltinLoweringContext`.", @@ -42385,7 +44925,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload` through `BuiltinLoweringContext`.", @@ -42506,7 +45046,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_call` through `BuiltinLoweringContext`.", @@ -42620,7 +45160,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_extensions` through `BuiltinLoweringContext`.", @@ -42714,7 +45254,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_functions` through `BuiltinLoweringContext`.", @@ -42819,7 +45359,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_register` through `BuiltinLoweringContext`.", @@ -42947,7 +45487,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_autoload_unregister` through `BuiltinLoweringContext`.", @@ -43054,7 +45594,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_classes` through `BuiltinLoweringContext`.", @@ -43161,7 +45701,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_object_hash` through `BuiltinLoweringContext`.", @@ -43262,7 +45802,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.spl_object_id` through `BuiltinLoweringContext`.", @@ -43362,7 +45902,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sprintf` through `BuiltinLoweringContext`.", @@ -43463,7 +46003,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sqrt` through `BuiltinLoweringContext`.", @@ -43566,7 +46106,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sscanf` through `BuiltinLoweringContext`.", @@ -43688,7 +46228,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stat` through `BuiltinLoweringContext`.", @@ -43809,7 +46349,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_contains` through `BuiltinLoweringContext`.", @@ -43919,7 +46459,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_ends_with` through `BuiltinLoweringContext`.", @@ -44041,7 +46581,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_ireplace` through `BuiltinLoweringContext`.", @@ -44177,7 +46717,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_pad` through `BuiltinLoweringContext`.", @@ -44197,7 +46737,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -44301,7 +46843,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_repeat` through `BuiltinLoweringContext`.", @@ -44321,7 +46863,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -44423,7 +46967,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_replace` through `BuiltinLoweringContext`.", @@ -44547,7 +47091,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_split` through `BuiltinLoweringContext`.", @@ -44567,7 +47111,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -44658,7 +47204,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.str_starts_with` through `BuiltinLoweringContext`.", @@ -44729,6 +47275,132 @@ "slug": "str_starts_with", "sub_area": "String" }, + { + "area": "String", + "canonical_name": "str_word_count", + "description": "Counts the words in a string, or returns them as a list or byte-offset map.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_word_count.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "format", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.str_word_count` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/str_word_count.rs", + "sig_line": null + }, + "name": "str_word_count", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "str_word_count" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "str_word_count" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "format", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "characters", + "optional": true, + "type": "string" + } + ], + "return_type": "array|int", + "variadic": null + }, + "slug": "str_word_count", + "sub_area": "String" + }, { "area": "String", "canonical_name": "strcasecmp", @@ -44768,7 +47440,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strcasecmp` through `BuiltinLoweringContext`.", @@ -44878,7 +47550,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strcmp` through `BuiltinLoweringContext`.", @@ -44988,7 +47660,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_append` through `BuiltinLoweringContext`.", @@ -45109,7 +47781,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_make_writeable` through `BuiltinLoweringContext`.", @@ -45229,7 +47901,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_new` through `BuiltinLoweringContext`.", @@ -45356,7 +48028,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_bucket_prepend` through `BuiltinLoweringContext`.", @@ -45483,7 +48155,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_create` through `BuiltinLoweringContext`.", @@ -45605,7 +48277,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_default` through `BuiltinLoweringContext`.", @@ -45720,7 +48392,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_options` through `BuiltinLoweringContext`.", @@ -45835,7 +48507,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_get_params` through `BuiltinLoweringContext`.", @@ -45950,7 +48622,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_default` through `BuiltinLoweringContext`.", @@ -46083,7 +48755,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_option` through `BuiltinLoweringContext`.", @@ -46224,7 +48896,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_context_set_params` through `BuiltinLoweringContext`.", @@ -46363,7 +49035,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_copy_to_stream` through `BuiltinLoweringContext`.", @@ -46517,7 +49189,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_append` through `BuiltinLoweringContext`.", @@ -46670,7 +49342,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_prepend` through `BuiltinLoweringContext`.", @@ -46811,7 +49483,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_register` through `BuiltinLoweringContext`.", @@ -46933,7 +49605,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_filter_remove` through `BuiltinLoweringContext`.", @@ -47060,7 +49732,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_contents` through `BuiltinLoweringContext`.", @@ -47182,7 +49854,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_filters` through `BuiltinLoweringContext`.", @@ -47301,7 +49973,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_line` through `BuiltinLoweringContext`.", @@ -47430,7 +50102,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_meta_data` through `BuiltinLoweringContext`.", @@ -47538,7 +50210,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_transports` through `BuiltinLoweringContext`.", @@ -47638,7 +50310,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_get_wrappers` through `BuiltinLoweringContext`.", @@ -47745,7 +50417,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_is_local` through `BuiltinLoweringContext`.", @@ -47859,7 +50531,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_isatty` through `BuiltinLoweringContext`.", @@ -47974,7 +50646,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_resolve_include_path` through `BuiltinLoweringContext`.", @@ -48111,7 +50783,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_select` through `BuiltinLoweringContext`.", @@ -48259,7 +50931,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_blocking` through `BuiltinLoweringContext`.", @@ -48387,7 +51059,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_chunk_size` through `BuiltinLoweringContext`.", @@ -48514,7 +51186,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_read_buffer` through `BuiltinLoweringContext`.", @@ -48647,7 +51319,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_timeout` through `BuiltinLoweringContext`.", @@ -48782,7 +51454,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_set_write_buffer` through `BuiltinLoweringContext`.", @@ -48914,7 +51586,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_accept` through `BuiltinLoweringContext`.", @@ -49043,7 +51715,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_client` through `BuiltinLoweringContext`.", @@ -49176,7 +51848,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_enable_crypto` through `BuiltinLoweringContext`.", @@ -49323,7 +51995,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_get_name` through `BuiltinLoweringContext`.", @@ -49457,7 +52129,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_pair` through `BuiltinLoweringContext`.", @@ -49602,7 +52274,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_recvfrom` through `BuiltinLoweringContext`.", @@ -49756,7 +52428,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_sendto` through `BuiltinLoweringContext`.", @@ -49892,7 +52564,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_server` through `BuiltinLoweringContext`.", @@ -50013,7 +52685,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_socket_shutdown` through `BuiltinLoweringContext`.", @@ -50135,7 +52807,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_supports_lock` through `BuiltinLoweringContext`.", @@ -50262,7 +52934,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_register` through `BuiltinLoweringContext`.", @@ -50391,7 +53063,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_restore` through `BuiltinLoweringContext`.", @@ -50505,7 +53177,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.stream_wrapper_unregister` through `BuiltinLoweringContext`.", @@ -50586,6 +53258,132 @@ "slug": "stream_wrapper_unregister", "sub_area": "IO" }, + { + "area": "String", + "canonical_name": "stripos", + "description": "Finds the numeric position of the first case-insensitive occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stripos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.stripos` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/stripos.rs", + "sig_line": null + }, + "name": "stripos", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "stripos" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "stripos" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "stripos", + "sub_area": "String" + }, { "area": "String", "canonical_name": "stripslashes", @@ -50619,7 +53417,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.strip_slashes` through `BuiltinLoweringContext`.", @@ -50713,7 +53511,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_graph` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -50771,6 +53569,200 @@ "slug": "strlen", "sub_area": "String" }, + { + "area": "String", + "canonical_name": "strncasecmp", + "description": "Compares the first n bytes of two strings, ignoring ASCII case.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.strncasecmp` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/strncasecmp.rs", + "sig_line": null + }, + "name": "strncasecmp", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "strncasecmp" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "strncasecmp" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "strncasecmp", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "strncmp", + "description": "Compares the first n bytes of two strings.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.strncmp` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/strncmp.rs", + "sig_line": null + }, + "name": "strncmp", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "strncmp" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "strncmp" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false, + "type": "int" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "strncmp", + "sub_area": "String" + }, { "area": "String", "canonical_name": "strpos", @@ -50816,7 +53808,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strpos` through `BuiltinLoweringContext`.", @@ -50836,7 +53828,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -50892,101 +53886,227 @@ "return_type": "mixed", "variadic": null }, - "slug": "strpos", - "sub_area": "String" - }, - { - "area": "String", - "canonical_name": "strrev", - "description": "Reverses a string.", - "eval": { - "area": "string", - "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrev.rs", - "hooks": [ - "direct", - "values" - ], - "kind": "registry", - "params": [ - { - "by_ref": false, - "default": null, - "name": "string", - "optional": false - } - ], - "required_param_count": 1, - "supported": true, - "variadic": null - }, - "eval_only": false, - "in_catalog": true, - "is_extension": false, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/builtins/semantics.rs", - "codegen_function": "lower_registry_call", - "codegen_line": 448, - "notes": [ - "Uses the `runtime_call` strategy from the single-source builtin descriptor.", - "Emits the typed EIR target `runtime.string.reverse` through `BuiltinLoweringContext`.", - "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." - ], - "runtime_helpers": [], - "sig_arm": null, - "sig_file": "src/builtins/string/strrev.rs", - "sig_line": null - }, - "name": "strrev", - "semantics": { - "argument_lowering": "standard", - "callable": { - "kind": "dynamic" - }, - "effects": { - "kind": "static", - "names": [] - }, - "lowering": { - "kind": "runtime_call", - "target": "string.reverse" - }, - "ownership": { - "argument_indexes": [], - "kind": "fresh" - }, - "requirements": { - "kind": "static", - "values": [] - }, - "result_type": "declared", - "runtime_functions": [], - "target_strategy": "runtime_call", - "target_support": [ - "macos-aarch64", - "linux-aarch64", - "linux-x86_64" - ], - "validation": { - "kind": "signature" - } - }, - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "string", - "optional": false, - "type": "string" - } - ], - "return_type": "string", - "variadic": null - }, - "slug": "strrev", + "slug": "strpos", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "strrev", + "description": "Reverses a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrev.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.string.reverse` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/strrev.rs", + "sig_line": null + }, + "name": "strrev", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "dynamic" + }, + "effects": { + "kind": "static", + "names": [] + }, + "lowering": { + "kind": "runtime_call", + "target": "string.reverse" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "strrev", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "strripos", + "description": "Finds the numeric position of the last case-insensitive occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strripos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.strripos` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/strripos.rs", + "sig_line": null + }, + "name": "strripos", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "strripos" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "strripos" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "strripos", "sub_area": "String" }, { @@ -51034,7 +54154,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strrpos` through `BuiltinLoweringContext`.", @@ -51054,7 +54174,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -51158,7 +54280,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strstr` through `BuiltinLoweringContext`.", @@ -51270,7 +54392,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.to_lower` through `BuiltinLoweringContext`.", @@ -51370,7 +54492,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.strtotime` through `BuiltinLoweringContext`.", @@ -51492,7 +54614,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.to_upper` through `BuiltinLoweringContext`.", @@ -51553,6 +54675,133 @@ "slug": "strtoupper", "sub_area": "String" }, + { + "area": "String", + "canonical_name": "strtr", + "description": "Translates bytes pairwise, or applies longest-match-first replacement pairs.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strtr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "to", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.strtr` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/strtr.rs", + "sig_line": null + }, + "name": "strtr", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "reads_heap", + "alloc_concat" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "strtr" + }, + "ownership": { + "argument_indexes": [], + "kind": "fresh" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "checked", + "runtime_functions": [ + "strtr" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "checker_hook", + "lazy": false + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false, + "type": "array|string" + }, + { + "by_ref": false, + "default": "null", + "name": "to", + "optional": true, + "type": "string" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "strtr", + "sub_area": "String" + }, { "area": "Type", "canonical_name": "strval", @@ -51586,7 +54835,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `eir_primitive` strategy from the single-source builtin descriptor.", "Emits backend-neutral EIR primitives or a small EIR graph through `BuiltinLoweringContext`." @@ -51690,7 +54939,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.substr` through `BuiltinLoweringContext`.", @@ -51768,6 +55017,110 @@ "slug": "substr", "sub_area": "String" }, + { + "area": "String", + "canonical_name": "substr_count", + "description": "Counts the number of non-overlapping substring occurrences.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, + "in_catalog": true, + "is_extension": false, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/builtins/semantics.rs", + "codegen_function": "lower_registry_call", + "codegen_line": 540, + "notes": [ + "Uses the `runtime_call` strategy from the single-source builtin descriptor.", + "Emits the typed EIR target `runtime.substr_count` through `BuiltinLoweringContext`.", + "The backend resolves that typed target through `src/codegen/lower_inst/runtime_calls.rs`; PHP builtin names do not participate in dispatch." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/string/substr_count.rs", + "sig_line": null + }, + "name": "substr_count", + "semantics": { + "argument_lowering": "standard", + "callable": { + "kind": "static_only", + "reason": "typed backend operation has no runtime-selected wrapper contract" + }, + "effects": { + "kind": "static", + "names": [ + "may_throw" + ] + }, + "lowering": { + "kind": "runtime_call", + "target": "substr_count" + }, + "ownership": { + "argument_indexes": [], + "kind": "may_alias_arguments" + }, + "requirements": { + "kind": "static", + "values": [] + }, + "result_type": "declared", + "runtime_functions": [ + "substr_count" + ], + "target_strategy": "runtime_call", + "target_support": [ + "macos-aarch64", + "linux-aarch64", + "linux-x86_64" + ], + "validation": { + "kind": "signature" + } + }, + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true, + "type": "mixed" + } + ], + "return_type": "int", + "variadic": null + }, + "slug": "substr_count", + "sub_area": "String" + }, { "area": "String", "canonical_name": "substr_replace", @@ -51819,7 +55172,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.substr_replace` through `BuiltinLoweringContext`.", @@ -51943,7 +55296,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.symlink` through `BuiltinLoweringContext`.", @@ -52057,7 +55410,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.sys_get_temp_dir` through `BuiltinLoweringContext`.", @@ -52163,7 +55516,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.system` through `BuiltinLoweringContext`.", @@ -52277,7 +55630,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tan` through `BuiltinLoweringContext`.", @@ -52374,7 +55727,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tanh` through `BuiltinLoweringContext`.", @@ -52477,7 +55830,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tempnam` through `BuiltinLoweringContext`.", @@ -52591,7 +55944,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.time` through `BuiltinLoweringContext`.", @@ -52675,7 +56028,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.tmpfile` through `BuiltinLoweringContext`.", @@ -52794,7 +56147,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.touch` through `BuiltinLoweringContext`.", @@ -52929,7 +56282,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.trait_exists` through `BuiltinLoweringContext`.", @@ -53057,7 +56410,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.trim` through `BuiltinLoweringContext`.", @@ -53166,7 +56519,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.uasort` through `BuiltinLoweringContext`.", @@ -53288,7 +56641,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ucfirst` through `BuiltinLoweringContext`.", @@ -53391,7 +56744,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.ucwords` through `BuiltinLoweringContext`.", @@ -53500,7 +56853,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.uksort` through `BuiltinLoweringContext`.", @@ -53563,7 +56916,7 @@ ], "validation": { "kind": "checker_hook", - "lazy": false + "lazy": true } }, "sig": { @@ -53622,7 +56975,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.umask` through `BuiltinLoweringContext`.", @@ -53736,7 +57089,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.unlink` through `BuiltinLoweringContext`.", @@ -53834,7 +57187,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.unserialize` through `BuiltinLoweringContext`.", @@ -53956,9 +57309,22 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_unset_builtin", - "codegen_line": 49, + "codegen_line": 140, "notes": [ - "Rejects `unset()` calls that were not converted into direct EIR unbind operations." + "Rejects `unset()` calls that were not converted into direct EIR unbind operations.", + "Reaching this lowering means `crate::ir_lower::expr` could not turn the target", + "into a slot clear, a hash/array removal, an `offsetUnset()` call, a `__unset()`", + "call or a dynamic-property removal, so the message lists the shapes that do lower", + "directly and then names the one shape users hit most.", + "THE UNTYPED FIXED SLOT is that shape. `unset($obj->untypedProp)` on a property", + "declared without a type (`public $foo = 1;`) truly REMOVES it in PHP: a later read", + "warns `Undefined property` and answers `null`, and a later write recreates it.", + "elephc gives each declared property a fixed, monomorphically typed slot, so a", + "property the checker typed `Int` has no encoding for \"removed and reading as null\"", + "\u2014 every candidate encoding answers `int(0)` or a raw marker word instead. A loud", + "error beats a wrong value, so the shape is refused here. Untyped properties whose", + "storage is a DYNAMIC hash (`stdClass`, undeclared names on", + "`#[AllowDynamicProperties]` classes) are genuinely removable and lower fine." ], "runtime_helpers": [], "sig_arm": null, @@ -54016,7 +57382,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.url_decode` through `BuiltinLoweringContext`.", @@ -54110,7 +57476,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.string.url_encode` through `BuiltinLoweringContext`.", @@ -54204,7 +57570,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.usleep` through `BuiltinLoweringContext`.", @@ -54308,7 +57674,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.usort` through `BuiltinLoweringContext`.", @@ -54430,7 +57796,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.var_dump` through `BuiltinLoweringContext`.", @@ -54556,7 +57922,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vfprintf` through `BuiltinLoweringContext`.", @@ -54691,7 +58057,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vprintf` through `BuiltinLoweringContext`.", @@ -54818,7 +58184,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.vsprintf` through `BuiltinLoweringContext`.", @@ -54944,7 +58310,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.wordwrap` through `BuiltinLoweringContext`.", @@ -54964,7 +58330,9 @@ }, "effects": { "kind": "static", - "names": [] + "names": [ + "may_throw" + ] }, "lowering": { "kind": "runtime_call", @@ -55046,7 +58414,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_free` through `BuiltinLoweringContext`.", @@ -55145,7 +58513,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_pack` through `BuiltinLoweringContext`.", @@ -55244,7 +58612,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_type` through `BuiltinLoweringContext`.", @@ -55343,7 +58711,7 @@ "checker_line": null, "codegen_file": "src/builtins/semantics.rs", "codegen_function": "lower_registry_call", - "codegen_line": 448, + "codegen_line": 540, "notes": [ "Uses the `runtime_call` strategy from the single-source builtin descriptor.", "Emits the typed EIR target `runtime.zval_unpack` through `BuiltinLoweringContext`.", diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index 46b1a91e5f..22447a4f30 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -314,6 +314,9 @@ "str_repeat": ["string", "int"], "str_pad": ["string", "int", "string", "int"], "str_split": ["string", "int"], + # `$from` is `array|string` in reference PHP; the registry records the coarse `Mixed` + # that covers both call shapes. + "strtr": ["string", "array|string", "string"], "str_contains": ["string", "string"], "str_starts_with": ["string", "string"], "str_ends_with": ["string", "string"], @@ -1252,6 +1255,10 @@ def slug(name: str) -> str: "uasort": "bool", "uksort": "bool", "usort": "bool", + # `$format`/`$mode` select the result shape at run time in reference PHP; the registry + # records the coarse `Mixed` that covers both shapes for the whole arity. + "str_word_count": "array|int", + "count_chars": "array|string", # Other concrete return types confirmed by PHP reflection. "class_alias": "bool", "define": "bool", diff --git a/src/autoload/alias.rs b/src/autoload/alias.rs index 6cd86744e0..4003b71860 100644 --- a/src/autoload/alias.rs +++ b/src/autoload/alias.rs @@ -52,6 +52,7 @@ fn collect_aliases_in_stmt(stmt: Stmt, alias_decls: &mut Vec) -> Option Some(Stmt { @@ -61,6 +62,7 @@ fn collect_aliases_in_stmt(stmt: Stmt, alias_decls: &mut Vec) -> Option Some(Stmt { @@ -70,18 +72,21 @@ fn collect_aliases_in_stmt(stmt: Stmt, alias_decls: &mut Vec) -> Option Some(Stmt { kind: StmtKind::Synthetic(collect_aliases_in_top_level(body, alias_decls)), span, source_mode, + strict_types, attributes, }), kind => Some(Stmt { kind, span, source_mode, + strict_types, attributes, }), } diff --git a/src/builtins/array/array_chunk.rs b/src/builtins/array/array_chunk.rs index 3dc3d62e06..6476937a59 100644 --- a/src/builtins/array/array_chunk.rs +++ b/src/builtins/array/array_chunk.rs @@ -5,21 +5,26 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - `check` reproduces the legacy rule: chunking an indexed `Array` yields a -//! nested `Array>`. Associative inputs are rejected (the lowering only -//! supports indexed arrays), and non-array inputs are rejected too. A check hook is -//! required because the return type depends on the inferred argument type. -//! - Arity (exactly 2 arguments) is validated by the registry's `check_arity` before -//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - PHP's signature is `array_chunk(array $array, int $length, bool $preserve_keys = false)`; +//! both the positional and the `preserve_keys:` named form are accepted. +//! - `preserve_keys` CHANGES THE RESULT SHAPE, so it must be a literal in AOT mode (same rule as +//! `array_reverse()`'s and `array_slice()`'s flags). With `false` an indexed `Array` chunks +//! into `Array>`; with `true` each chunk keeps the source integer keys of its own +//! window, which is `Array` because elephc's dense indexed +//! representation cannot hold a window that does not start at key 0. +//! - Associative inputs are rejected (the lowering only supports indexed arrays), and non-array +//! inputs are rejected too. A check hook is required because the return type depends on the +//! inferred argument type and on that literal flag. -use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; builtin! { name: "array_chunk", area: Array, - params: [array: Mixed, length: Mixed], + params: [array: Mixed, length: Mixed, preserve_keys: Bool = DefaultSpec::Bool(false)], returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -29,15 +34,42 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.array-chunk.php", } +/// Reads a literal `preserve_keys` flag, returning `None` when the argument is not a literal. +/// +/// An absent argument reads as a literal `false` so callers can treat "omitted" and "explicit +/// false" identically. Integer literals follow PHP truthiness, matching `array_reverse()`. +fn literal_preserve_keys(flag: Option<&Expr>) -> Option { + match flag { + None => Some(false), + Some(flag) => match flag.kind { + ExprKind::BoolLiteral(value) => Some(value), + ExprKind::IntLiteral(value) => Some(value != 0), + _ => None, + }, + } +} + /// Returns the nested chunk-array type for an `array_chunk` call. /// -/// An indexed `Array` chunks into `Array>`. Associative arrays are -/// rejected (only indexed arrays are supported), and non-array arguments are rejected. -/// The argument is re-inferred here to drive the return type; the registry already -/// inferred every argument once for side effects, and arity (exactly 2) is pre-validated. +/// An indexed `Array` chunks into `Array>`, or into +/// `Array` when a literal `preserve_keys: true` keeps each +/// window's source integer keys. Associative arrays are rejected (only indexed arrays are +/// supported), non-array arguments are rejected, and so is a non-literal flag. The first argument +/// is re-inferred here to drive the return type; the registry already inferred every argument once +/// for side effects, and arity (2 or 3) is pre-validated. fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let preserve = literal_preserve_keys(cx.args.get(2)).ok_or_else(|| { + CompileError::new( + cx.span, + "array_chunk() preserve_keys argument must be a literal bool in AOT mode", + ) + })?; match ty { + PhpType::Array(elem_ty) if preserve => Ok(PhpType::Array(Box::new(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: elem_ty, + }))), PhpType::Array(elem_ty) => Ok(PhpType::Array(Box::new(PhpType::Array(elem_ty)))), PhpType::AssocArray { .. } => Err(CompileError::new( cx.span, diff --git a/src/builtins/array/array_count_values.rs b/src/builtins/array/array_count_values.rs new file mode 100644 index 0000000000..44a9835ee4 --- /dev/null +++ b/src/builtins/array/array_count_values.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Home of the PHP `array_count_values` builtin: its single-source registry declaration and +//! semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - `check` is required because the return type depends on the argument: the source VALUES +//! become the result KEYS, so the result is `AssocArray`. The `Int` +//! value type is fixed — every entry is an occurrence tally. +//! - php-src warns (`E_WARNING`) and SKIPS any element that is neither int nor string, so a +//! heterogeneous source is still accepted at compile time; the runtime helper emits the +//! warning. +//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before the hook +//! fires. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::{array_key_type_from_value_type, PhpType}; + +builtin! { + name: "array_count_values", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ArrayCountValues, + ), + summary: "Counts the occurrences of each distinct value in an array.", + php_manual: "https://www.php.net/manual/en/function.array-count-values.php", +} + +/// Returns the tally associative-array type for an `array_count_values` call. +/// +/// Source values become result keys, so the key type is derived from the source element/value +/// type via `array_key_type_from_value_type`; the value type is always `Int`. The argument is +/// re-inferred here to drive the return type, and arity is pre-validated by the registry. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(elem) => Ok(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(*elem)), + value: Box::new(PhpType::Int), + }), + PhpType::AssocArray { value, .. } => Ok(PhpType::AssocArray { + key: Box::new(array_key_type_from_value_type(*value)), + value: Box::new(PhpType::Int), + }), + _ => Err(CompileError::new( + cx.span, + "array_count_values() argument must be array", + )), + } +} diff --git a/src/builtins/array/array_reverse.rs b/src/builtins/array/array_reverse.rs index 2152c7d63d..cf560cfc98 100644 --- a/src/builtins/array/array_reverse.rs +++ b/src/builtins/array/array_reverse.rs @@ -5,20 +5,24 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - `check` reproduces the legacy rule: reversing preserves the array shape, so the -//! return type is the (array-or-assoc) input type unchanged. A check hook is -//! required both to reject non-array arguments and to echo the input type back. -//! - Arity (exactly 1 argument) is validated by the registry's `check_arity` before -//! the hook fires; the inline arity check from the legacy arm is not reproduced here. +//! - PHP's signature is `array_reverse(array $array, bool $preserve_keys = false)`; both the +//! positional and the `preserve_keys:` named form are accepted. +//! - `preserve_keys` CHANGES THE RESULT SHAPE, so it must be a literal in AOT mode (same rule as +//! `class_exists()`'s autoload flag). With `false` the result is the input array type; with +//! `true` an indexed `array` becomes `AssocArray { key: Int, value: T }`, because PHP keeps +//! the original integer keys while reversing the iteration order — something elephc's dense +//! indexed representation cannot express. +//! - `check` is required both to reject non-array arguments and to compute that shape. -use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; +use crate::parser::ast::ExprKind; use crate::types::PhpType; builtin! { name: "array_reverse", area: Array, - params: [array: Mixed], + params: [array: Mixed, preserve_keys: Bool = DefaultSpec::Bool(false)], returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -28,11 +32,14 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.array-reverse.php", } -/// Returns the (shape-preserving) array type for an `array_reverse` call. +/// Returns the reversed array's type, which depends on the literal `preserve_keys` flag. /// -/// Reversing keeps the array shape, so the input array/assoc type is returned -/// unchanged. Non-array arguments are rejected. The argument is re-inferred here; -/// the registry already inferred it once for side effects, and arity is pre-validated. +/// Without `preserve_keys` (or with a literal `false`) reversing keeps the array shape, so the +/// input array/assoc type is returned unchanged. With a literal `true` an indexed array keeps its +/// integer keys in reversed insertion order, which is an `AssocArray` keyed by `Int`; a source +/// that is already associative keeps its own shape because reordering a hash preserves its keys. +/// Non-array arguments and a non-literal flag are rejected. Arity is pre-validated and every +/// argument has already been inferred once by the registry's common path. fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { @@ -41,5 +48,27 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { "array_reverse() argument must be array", )); } - Ok(ty) + let Some(flag) = cx.args.get(1) else { + return Ok(ty); + }; + let preserve = match flag.kind { + ExprKind::BoolLiteral(value) => value, + ExprKind::IntLiteral(value) => value != 0, + _ => { + return Err(CompileError::new( + cx.span, + "array_reverse() preserve_keys argument must be a literal bool in AOT mode", + )) + } + }; + if !preserve { + return Ok(ty); + } + match ty { + PhpType::Array(elem) => Ok(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: elem, + }), + other => Ok(other), + } } diff --git a/src/builtins/array/array_search.rs b/src/builtins/array/array_search.rs index e81d4c4c80..1d8432e9aa 100644 --- a/src/builtins/array/array_search.rs +++ b/src/builtins/array/array_search.rs @@ -7,12 +7,10 @@ //! Key details: //! - `check` validates the second argument is an array and returns a union of the //! key type and Bool (false on not-found), or Int|Bool for indexed arrays. -//! - The golden signature carries the optional `strict` param (min=2, max=3), but the -//! legacy CHECK arm enforced exactly 2 arguments and the `lower_array_search` emitter -//! only supports 2 args. `max_args: 2` reproduces that exact-2 enforcement in -//! `check_arity` only; `function_sig` and the parity gate keep the full param-derived -//! bounds from the golden. This keeps the clean "takes exactly 2 arguments" checker -//! diagnostic for a 3-arg call instead of an EIR backend error. +//! - The full PHP signature is `array_search(mixed $needle, array $haystack, bool $strict = false)` +//! (min=2, max=3) and is enforced verbatim: no `max_args` override narrows it. `strict` +//! works positionally and as a named argument, and is honoured by `lower_array_search`. +//! - `strict` does not change the result type: PHP still returns the found key or `false`. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; @@ -22,7 +20,6 @@ builtin! { name: "array_search", area: Array, params: [needle: Mixed, haystack: Mixed, strict: Bool = DefaultSpec::Bool(false)], - max_args: 2, returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -34,9 +31,9 @@ builtin! { /// Validates haystack is an array and returns the key-or-false union type. /// -/// The registry's `check_arity` handles arity enforcement (capped at 2 by `max_args` -/// to match the legacy CHECK arm). For assoc arrays the return is `key_type | bool`; -/// for indexed arrays it is `int | bool`. +/// The registry's `check_arity` handles arity enforcement (2 or 3 arguments) and infers every +/// argument, including the optional `strict` flag, before this hook runs. For assoc arrays the +/// return is `key_type | bool`; for indexed arrays it is `int | bool`. fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_type(&cx.args[0], cx.env)?; let arr_ty = cx.checker.infer_type(&cx.args[1], cx.env)?; diff --git a/src/builtins/array/array_slice.rs b/src/builtins/array/array_slice.rs index b5d437221f..7b8acb49f0 100644 --- a/src/builtins/array/array_slice.rs +++ b/src/builtins/array/array_slice.rs @@ -5,55 +5,93 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - `check` reproduces the legacy rule: a slice preserves the array shape, so the -//! return type is the (array-or-assoc) input type unchanged; a boxed `Mixed`/`Union` -//! input yields `Mixed`. A check hook is required because the return type depends on -//! the inferred first-argument type. -//! - The declared signature carries the golden param list (`array`, `offset`, -//! `length`), with `length` optional (default `null`), so the registry's -//! `check_arity` accepts 2 or 3 arguments — matching the legacy CHECK arm. +//! - PHP's signature is +//! `array_slice(array $array, int $offset, ?int $length = null, bool $preserve_keys = false)`; +//! every parameter is accepted positionally and by name. +//! - `preserve_keys` CHANGES THE RESULT SHAPE, so it must be a literal in AOT mode (same rule as +//! `array_reverse()`'s flag). With `false` a slice preserves the array shape, so the return type +//! is the (array-or-assoc) input type unchanged. With `true` an indexed `array` becomes +//! `AssocArray { key: Int, value: T }`, because PHP keeps the source integer keys of the +//! selected window — something elephc's dense indexed representation cannot express. +//! - The result shape depends on an argument VALUE, not on argument types, so it must travel on +//! the `Checked` contract: a `Shared` resolver is re-run by `semantics::lower_registry_call` +//! with `args: &[]` and could not see the flag there. A boxed `Mixed`/`Union` source therefore +//! reports `array` — the exact layout `lower_mixed_array_slice` materializes — rather +//! than a bare `Mixed`, which is also PHP-accurate because `array_slice()` always returns an +//! array. `RuntimeFnId::ArraySlice::fallback_result_type` supplies the same layout for +//! synthetic call sites with no checked type. +//! - The checked type below is a CHECKER type: call-site specialization narrows an untyped +//! parameter (`function top($scores)`) that EIR still lowers under the boxed-`Mixed` ABI +//! contract, so the type recorded here can be narrower than the operand the slice helper +//! actually copies. `RuntimeFnId::ArraySlice::checked_result_type_fits_operands` rejects such a +//! type during EIR lowering so the boxed-`Mixed` layout above is used instead. +//! - `check` is required both to reject non-array arguments and to compute that shape. use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; -use crate::builtins::semantics::{ - runtime_fn_semantics, BuiltinResultType, BuiltinSemanticInput, BuiltinSemantics, -}; use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; builtin! { name: "array_slice", area: Array, - params: [array: Mixed, offset: Mixed, length: Mixed = DefaultSpec::Null], + params: [ + array: Mixed, + offset: Mixed, + length: Mixed = DefaultSpec::Null, + preserve_keys: Bool = DefaultSpec::Bool(false) + ], returns: Mixed, check: check, - semantics: array_slice_semantics(), + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ArraySlice, + ), summary: "Extracts a slice of an array.", php_manual: "https://www.php.net/manual/en/function.array-slice.php", } -/// Builds semantics with the boxed-Mixed indexed result layout used by the slice runtime. -const fn array_slice_semantics() -> BuiltinSemantics { - let mut semantics = runtime_fn_semantics(crate::ir::RuntimeFnId::ArraySlice); - semantics.result_type = BuiltinResultType::Shared(eir_result_type); - semantics -} - -/// Returns the representation-safe indexed array type for typed and boxed source arrays. -fn eir_result_type(_input: &BuiltinSemanticInput<'_>) -> PhpType { - PhpType::Array(Box::new(PhpType::Mixed)) +/// Reads a literal `preserve_keys` flag, returning `None` when the argument is not a literal. +/// +/// An absent argument reads as a literal `false` so callers can treat "omitted" and "explicit +/// false" identically. Integer literals follow PHP truthiness, matching `array_reverse()`. +fn literal_preserve_keys(flag: Option<&Expr>) -> Option { + match flag { + None => Some(false), + Some(flag) => match flag.kind { + ExprKind::BoolLiteral(value) => Some(value), + ExprKind::IntLiteral(value) => Some(value != 0), + _ => None, + }, + } } /// Returns the slice's array type for an `array_slice` call. /// -/// A slice preserves the input array shape, so the (array-or-assoc) first-argument -/// type is returned unchanged; a boxed `Mixed`/`Union` first argument yields `Mixed`. -/// Non-array first arguments are rejected. The first argument is re-inferred here; -/// the registry already inferred every argument once for side effects, and arity -/// (2 or 3) is pre-validated by the registry. +/// Without `preserve_keys` a slice preserves the input array shape, so the (array-or-assoc) +/// first-argument type is returned unchanged and a boxed `Mixed`/`Union` first argument yields +/// `array`. With a literal `preserve_keys: true` an indexed source keeps the integer keys of +/// the selected window, which is an `AssocArray` keyed by `Int`; a source that is already associative +/// keeps its own shape because narrowing a hash preserves its keys. Non-array first arguments and +/// a non-literal flag are rejected, and so is a key-preserving slice of a boxed `Mixed` array, +/// whose element layout is not statically known. The first argument is re-inferred here; the +/// registry already inferred every argument once for side effects, and arity (2 to 4) is +/// pre-validated by the registry. fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let preserve = literal_preserve_keys(cx.args.get(3)).ok_or_else(|| { + CompileError::new( + cx.span, + "array_slice() preserve_keys argument must be a literal bool in AOT mode", + ) + })?; if matches!(ty, PhpType::Mixed | PhpType::Union(_)) { - return Ok(PhpType::Mixed); + if preserve { + return Err(CompileError::new( + cx.span, + "array_slice() preserve_keys requires a statically known array type", + )); + } + return Ok(PhpType::Array(Box::new(PhpType::Mixed))); } if !matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { return Err(CompileError::new( @@ -61,5 +99,14 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { "array_slice() first argument must be array", )); } - Ok(ty) + if !preserve { + return Ok(ty); + } + match ty { + PhpType::Array(elem) => Ok(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: elem, + }), + other => Ok(other), + } } diff --git a/src/builtins/array/array_splice.rs b/src/builtins/array/array_splice.rs index 5b5b2a5857..419ac268e5 100644 --- a/src/builtins/array/array_splice.rs +++ b/src/builtins/array/array_splice.rs @@ -5,10 +5,10 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - The golden signature is `first_param_ref(optional(["array","offset","length"], required=2, [null]))`: -//! 3 params, `array` by-ref, `length` optional with default null, arity 2-3. The `ref` marker -//! is mandatory — it is what makes by-reference mutation lower correctly (ir_lower reads -//! `ref_params` from the registry sig). +//! - The signature matches reference PHP 8.4 exactly: +//! `array_splice(array &$array, int $offset, ?int $length = null, mixed $replacement = [])`. +//! 4 params, `array` by-ref, arity 2-4. The `ref` marker is mandatory — it is what makes +//! by-reference mutation lower correctly (ir_lower reads `ref_params` from the registry sig). //! - `check` reproduces the legacy rule: `Mixed`/`Union` first arg yields `Mixed`; `Array` //! or `AssocArray` yields the first-arg type; any other type is an error. All remaining //! args are inferred for side effects. @@ -20,11 +20,17 @@ use crate::types::PhpType; builtin! { name: "array_splice", area: Array, - params: [ref array: Mixed, offset: Int, length: Mixed = DefaultSpec::Null], + params: [ + ref array: Mixed, + offset: Int, + length: Mixed = DefaultSpec::Null, + replacement: Mixed = DefaultSpec::EmptyArray + ], returns: Mixed, check: check, - semantics: crate::builtins::semantics::runtime_fn_semantics( - crate::ir::RuntimeFnId::ArraySplice, + semantics: crate::builtins::semantics::with_argument_lowering( + crate::builtins::semantics::runtime_fn_semantics(crate::ir::RuntimeFnId::ArraySplice), + crate::builtins::semantics::BuiltinArgumentLowering::ArraySplice, ), summary: "Removes a portion of the array and replaces it with something else.", php_manual: "https://www.php.net/manual/en/function.array-splice.php", @@ -32,7 +38,7 @@ builtin! { /// Returns the result type for an `array_splice` call. /// -/// Arity (2 or 3 args) is pre-validated by the registry. The first argument is re-inferred +/// Arity (2 to 4 args) is pre-validated by the registry. The first argument is re-inferred /// to drive the return type; remaining arguments are inferred for side effects. `Mixed` or /// `Union` first arguments yield `Mixed` (opaque path); `Array`/`AssocArray` yield the /// first-arg type; any other type is a compile error. diff --git a/src/builtins/array/array_unshift.rs b/src/builtins/array/array_unshift.rs index 2b0f0a2e18..b01b6bfa50 100644 --- a/src/builtins/array/array_unshift.rs +++ b/src/builtins/array/array_unshift.rs @@ -6,11 +6,13 @@ //! //! Key details: //! - The golden signature is `first_param_ref(variadic(["array"], "values"))`: `array` -//! by-ref plus a variadic `values` param. The legacy CHECK arm enforced exactly 2 -//! arguments, so `min_args: 2, max_args: 2` reproduce that enforcement in `check_arity` -//! only; `function_sig` and the parity gate keep the variadic shape from the golden. +//! by-ref plus a variadic `values` param. PHP accepts `array_unshift($a)` (no values, +//! returns the unchanged count) and any number of prepended values, so `min_args: 1` +//! is the only `check_arity` override; the maximum stays unbounded. //! - The `ref` marker on `array` is mandatory — it is what makes by-reference mutation //! lower correctly (ir_lower reads `ref_params` from the registry sig). +//! - `values` is variadic, so PHP rejects it as a named argument +//! (`array_unshift() does not accept unknown named parameters`); only `array` is nameable. //! - Returns `Int` — the new number of elements in the array. use crate::builtins::spec::BuiltinCheckCtx; @@ -22,8 +24,7 @@ builtin! { area: Array, params: [ref array: Mixed], variadic: "values", - min_args: 2, - max_args: 2, + min_args: 1, returns: Int, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -35,12 +36,14 @@ builtin! { /// Validates the first argument is an array for an `array_unshift` call. /// -/// Arity (exactly 2 args) is pre-validated by `check_arity`. Both arguments are inferred -/// to produce any side effects; the first must be an indexed or associative array or the -/// call is rejected. Returns `Int` — the new element count. +/// Arity (at least 1 arg) is pre-validated by `check_arity`. Every argument is inferred so +/// the prepended values still produce their side effects; the first must be an indexed or +/// associative array or the call is rejected. Returns `Int` — the new element count. fn check(cx: &mut BuiltinCheckCtx) -> Result { let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; - cx.checker.infer_type(&cx.args[1], cx.env)?; + for index in 1..cx.args.len() { + cx.checker.infer_type(&cx.args[index], cx.env)?; + } if !matches!(arr_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { return Err(CompileError::new( cx.span, diff --git a/src/builtins/array/array_walk.rs b/src/builtins/array/array_walk.rs index 6d6e1ef98c..94b173f7d3 100644 --- a/src/builtins/array/array_walk.rs +++ b/src/builtins/array/array_walk.rs @@ -30,11 +30,15 @@ builtin! { /// Validates the array and callback arguments for an `array_walk` call. /// -/// Infers the array, derives its element type, and checks the callback signature contextually. +/// Infers the array and checks the callback contextually against its element type, adding the +/// array's key type as a second parameter when the callback declares `function ($value, $key)`. /// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. fn check(cx: &mut BuiltinCheckCtx) -> Result { let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; - let callback_arg_types = [crate::types::checker::builtins::array_element_type(&arr_ty)]; + let callback_arg_types = crate::types::checker::builtins::array_walk_callback_arg_types( + &arr_ty, + &cx.args[1], + ); crate::types::checker::builtins::check_array_callback_builtin_call( cx.checker, &cx.args[1], diff --git a/src/builtins/array/array_walk_recursive.rs b/src/builtins/array/array_walk_recursive.rs index 7d092d8c24..2d85188968 100644 --- a/src/builtins/array/array_walk_recursive.rs +++ b/src/builtins/array/array_walk_recursive.rs @@ -30,11 +30,15 @@ builtin! { /// Validates the array and callback arguments for an `array_walk_recursive` call. /// -/// Infers the array, derives its element type, and checks the callback signature contextually. +/// Infers the array and checks the callback contextually against its element type, adding the +/// array's key type as a second parameter when the callback declares `function ($value, $key)`. /// Arity (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. fn check(cx: &mut BuiltinCheckCtx) -> Result { let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; - let callback_arg_types = [crate::types::checker::builtins::array_element_type(&arr_ty)]; + let callback_arg_types = crate::types::checker::builtins::array_walk_callback_arg_types( + &arr_ty, + &cx.args[1], + ); crate::types::checker::builtins::check_array_callback_builtin_call( cx.checker, &cx.args[1], diff --git a/src/builtins/array/count.rs b/src/builtins/array/count.rs index 5d6cad7b9c..1c6825de0d 100644 --- a/src/builtins/array/count.rs +++ b/src/builtins/array/count.rs @@ -8,10 +8,9 @@ //! - `check` validates the argument type (Array, AssocArray, Mixed, Union-of-countable, or //! Countable Object) and returns `Int`. The Countable interface check delegates to //! `cx.checker.class_implements_interface`. -//! - `max_args: 1` reproduces the legacy checker's exactly-1 enforcement: `mode` has a -//! default so `min` derives to 1; capping `max` at 1 yields the standard -//! "count() takes exactly 1 argument" diagnostic. The 2-param golden is preserved for -//! FCC and parity. +//! - `$mode` accepts `COUNT_NORMAL` (`0`) and `COUNT_RECURSIVE` (`1`); anything else raises +//! PHP's catchable `ValueError`. The guard lives in the backend +//! (`codegen::lower_inst::builtins::lower_count`) because `$mode` may be a runtime value. //! - All accepted representations lower through typed `runtime.count` so a typed array //! value carrying the runtime null-container sentinel still raises PHP's catchable TypeError. @@ -28,7 +27,6 @@ builtin! { name: "count", area: Array, params: [value: Mixed, mode: Int = DefaultSpec::Int(0)], - max_args: 1, returns: Int, check: check, semantics: count_semantics(), @@ -47,12 +45,16 @@ const fn count_semantics() -> BuiltinSemantics { } /// Resolves count's intrinsic read/throw contract from the checked receiver representation. +/// +/// `MAY_THROW` is unconditional: besides the null-container `TypeError`, every call can raise +/// the `ValueError` for a `$mode` outside `COUNT_NORMAL`/`COUNT_RECURSIVE`, so the call must +/// never be treated as a removable pure call. fn effects(input: &BuiltinSemanticInput<'_>) -> crate::ir::Effects { match input.arg_types.first().map(PhpType::codegen_repr) { Some(PhpType::Array(_) | PhpType::AssocArray { .. }) => { crate::ir::Effects::READS_HEAP | crate::ir::Effects::MAY_THROW } - _ => crate::ir::RuntimeFnId::Count.effects(), + _ => crate::ir::RuntimeFnId::Count.effects() | crate::ir::Effects::MAY_THROW, } } @@ -60,8 +62,9 @@ fn effects(input: &BuiltinSemanticInput<'_>) -> crate::ir::Effects { /// /// Accepts Array, AssocArray, Mixed (heterogeneous arrays), a Union where every member /// is countable, or an Object that implements the `Countable` interface. Arity -/// enforcement (exactly 1 argument) is handled by the registry's `check_arity` via -/// `max_args: 1`. Returns a `CompileError` for non-countable types or non-Countable objects. +/// enforcement (1 or 2 arguments) is handled by the registry's `check_arity`; `$mode`'s +/// value range is a runtime `ValueError`, not a compile-time error, exactly like PHP. +/// Returns a `CompileError` for non-countable types or non-Countable objects. fn check(cx: &mut BuiltinCheckCtx) -> Result { let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; match &ty { diff --git a/src/builtins/array/current.rs b/src/builtins/array/current.rs new file mode 100644 index 0000000000..c258e6ad15 --- /dev/null +++ b/src/builtins/array/current.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `current` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `false` once the internal pointer has run past either end of the array. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "current", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::Current, + crate::ir::RuntimeFnId::ArrayPtrValue, + ), + summary: "Returns the element under the array's internal pointer.", + php_manual: "https://www.php.net/manual/en/function.current.php", +} + +/// Validates the receiver shape and type for `current()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "current") +} diff --git a/src/builtins/array/end.rs b/src/builtins/array/end.rs new file mode 100644 index 0000000000..2ba8b6b8ad --- /dev/null +++ b/src/builtins/array/end.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `end` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `false` for an empty array, which also leaves the pointer invalid. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "end", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::End, + crate::ir::RuntimeFnId::ArrayPtrSeek, + ), + summary: "Moves the array's internal pointer to the last element and returns it.", + php_manual: "https://www.php.net/manual/en/function.end.php", +} + +/// Validates the receiver shape and type for `end()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "end") +} diff --git a/src/builtins/array/internal_pointer.rs b/src/builtins/array/internal_pointer.rs new file mode 100644 index 0000000000..1931110467 --- /dev/null +++ b/src/builtins/array/internal_pointer.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Shared checker contract for PHP's internal-array-pointer builtins +//! (`key`, `current`, `next`, `prev`, `reset`, `end`). +//! +//! Called from: +//! - The `check` hook of each of the six home files in `crate::builtins::array`. +//! +//! Key details: +//! - This module declares no builtin of its own; it only holds the validation the six +//! home files share, so each of them keeps exactly one `builtin!` declaration. +//! - The receiver-shape rule lives here rather than in EIR lowering so the diagnostic is +//! a normal type-check error with a source span, next to the argument-type diagnostic. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +/// Validates one internal-array-pointer call and returns its `Mixed` result type. +/// +/// Two rules are enforced, in the order PHP itself would notice them: +/// +/// 1. The receiver must be a **plain variable**. elephc stores the internal pointer in a +/// hidden cursor slot beside the array local rather than inside the array header, so a +/// property, array element, call result, or any other expression has nowhere to keep a +/// cursor. Accepting those silently would hand back a cursor detached from the value +/// the program actually names, so they are a named compile error instead. +/// 2. The receiver must be array-typed. `Mixed` is allowed because heterogeneous arrays +/// are `Mixed` at compile time; the runtime helpers report `false`/`null` when a Mixed +/// payload turns out not to be a container. +/// +/// The registry's `check_arity` has already enforced the single-argument arity, so +/// `cx.args[0]` is present whenever this runs. +pub fn check_array_pointer_call( + cx: &mut BuiltinCheckCtx, + name: &str, +) -> Result { + if !matches!(cx.args[0].kind, ExprKind::Variable(_)) { + return Err(CompileError::new( + cx.span, + &format!( + "{}() argument must be an array variable: elephc keeps the internal array \ + pointer in a hidden slot beside the variable, so properties, array \ + elements, and call results have no pointer to move", + name, + ), + )); + } + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + if !matches!( + ty, + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Mixed + ) { + return Err(CompileError::new( + cx.span, + &format!("{}() argument must be array", name), + )); + } + Ok(PhpType::Mixed) +} diff --git a/src/builtins/array/key.rs b/src/builtins/array/key.rs new file mode 100644 index 0000000000..03039a0190 --- /dev/null +++ b/src/builtins/array/key.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `key` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `null` once the internal pointer has run past either end of the array. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "key", + area: Array, + params: [array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::Key, + crate::ir::RuntimeFnId::ArrayPtrKey, + ), + summary: "Returns the key of the element under the array's internal pointer.", + php_manual: "https://www.php.net/manual/en/function.key.php", +} + +/// Validates the receiver shape and type for `key()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "key") +} diff --git a/src/builtins/array/mod.rs b/src/builtins/array/mod.rs index 3f75926f3c..d3e2a2bcd7 100644 --- a/src/builtins/array/mod.rs +++ b/src/builtins/array/mod.rs @@ -18,6 +18,7 @@ pub mod array_any; pub mod array_chunk; pub mod array_column; pub mod array_combine; +pub mod array_count_values; pub mod array_diff; pub mod array_diff_assoc; pub mod array_diff_key; @@ -62,12 +63,19 @@ pub mod array_walk_recursive; pub mod arsort; pub mod asort; pub mod count; +pub mod current; +pub mod end; pub mod in_array; +pub mod internal_pointer; +pub mod key; pub mod krsort; pub mod ksort; pub mod natcasesort; pub mod natsort; +pub mod next; +pub mod prev; pub mod range; +pub mod reset; pub mod rsort; pub mod shuffle; pub mod sort; diff --git a/src/builtins/array/next.rs b/src/builtins/array/next.rs new file mode 100644 index 0000000000..b2c518d202 --- /dev/null +++ b/src/builtins/array/next.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `next` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `false` when the pointer steps past the last element, and the pointer stays invalid until `reset()`/`end()`. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "next", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::Next, + crate::ir::RuntimeFnId::ArrayPtrSeek, + ), + summary: "Advances the array's internal pointer and returns the new element.", + php_manual: "https://www.php.net/manual/en/function.next.php", +} + +/// Validates the receiver shape and type for `next()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "next") +} diff --git a/src/builtins/array/prev.rs b/src/builtins/array/prev.rs new file mode 100644 index 0000000000..cd5f8100cb --- /dev/null +++ b/src/builtins/array/prev.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `prev` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `false` when the pointer steps before the first element, and the pointer stays invalid until `reset()`/`end()`. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "prev", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::Prev, + crate::ir::RuntimeFnId::ArrayPtrSeek, + ), + summary: "Rewinds the array's internal pointer and returns the new element.", + php_manual: "https://www.php.net/manual/en/function.prev.php", +} + +/// Validates the receiver shape and type for `prev()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "prev") +} diff --git a/src/builtins/array/range.rs b/src/builtins/array/range.rs index 4c3b2f9fe2..b48c57e506 100644 --- a/src/builtins/array/range.rs +++ b/src/builtins/array/range.rs @@ -5,16 +5,22 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - `check` infers both arguments and always returns `Array(Int)`. +//! - PHP's signature is `range($start, $end, int|float $step = 1)`; `step` works positionally and +//! as a named argument. +//! - `check` infers every argument and always returns `Array(Int)`: the supported endpoints and +//! step are integers, so the produced range is an indexed integer array. +//! - The three `ValueError`s php-src raises for a bad `$step` (zero, negative on an increasing +//! range, wider than the spanned interval) are runtime guards emitted by `lower_range`, because +//! the endpoints and the step can all be runtime values. -use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; use crate::types::PhpType; builtin! { name: "range", area: Array, - params: [start: Mixed, end: Mixed], + params: [start: Mixed, end: Mixed, step: Mixed = DefaultSpec::Int(1)], returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -24,13 +30,14 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.range.php", } -/// Infers both arguments and returns `Array(Int)`. +/// Infers every argument and returns `Array(Int)`. /// -/// The registry's `check_arity` handles arity enforcement (exactly 2 arguments). -/// Both arguments are inferred for side-effect tracking; the return type is always +/// The registry's `check_arity` handles arity enforcement (2 or 3 arguments). +/// All arguments are inferred for side-effect tracking; the return type is always /// an indexed integer array matching the runtime emitter's output shape. fn check(cx: &mut BuiltinCheckCtx) -> Result { - cx.checker.infer_type(&cx.args[0], cx.env)?; - cx.checker.infer_type(&cx.args[1], cx.env)?; + for index in 0..cx.args.len() { + cx.checker.infer_type(&cx.args[index], cx.env)?; + } Ok(PhpType::Array(Box::new(PhpType::Int))) } diff --git a/src/builtins/array/reset.rs b/src/builtins/array/reset.rs new file mode 100644 index 0000000000..57be17a1a0 --- /dev/null +++ b/src/builtins/array/reset.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Home of the PHP `reset` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - Returns `false` for an empty array, which also leaves the pointer invalid. +//! - The receiver's internal pointer lives in a compiler-allocated cursor slot beside the +//! array local, so the argument must be a plain variable. Both that rule and the +//! argument-type rule are shared with the other five pointer builtins in +//! `crate::builtins::array::internal_pointer`. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "reset", + area: Array, + params: [ref array: Mixed], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::array_pointer_semantics( + crate::builtins::semantics::ArrayPointerOp::Reset, + crate::ir::RuntimeFnId::ArrayPtrSeek, + ), + summary: "Rewinds the array's internal pointer to the first element and returns it.", + php_manual: "https://www.php.net/manual/en/function.reset.php", +} + +/// Validates the receiver shape and type for `reset()` and returns `Mixed`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + super::internal_pointer::check_array_pointer_call(cx, "reset") +} diff --git a/src/builtins/array/uksort.rs b/src/builtins/array/uksort.rs index fdf80fe770..182b9243fe 100644 --- a/src/builtins/array/uksort.rs +++ b/src/builtins/array/uksort.rs @@ -8,13 +8,12 @@ //! - The golden signature is `first_param_ref(fixed(["array", "callback"]))`: exactly 2 //! arguments, the `array` param is by-reference. The `ref` marker drives in-place //! mutation (ir_lower reads `ref_params` from the registry sig). -//! - `check` validates the comparator with two integer dummy arguments — `uksort` compares -//! array keys (always integer in the supported subset), not values. Returns `Void`. +//! - `check` derives the comparator parameter type from the array's KEY type — `uksort` +//! compares array keys, not values — so an unannotated comparator over a string-keyed +//! array types its parameters as `Str`. Returns `Void`. use crate::builtins::spec::BuiltinCheckCtx; use crate::errors::CompileError; -use crate::parser::ast::{Expr, ExprKind}; -use crate::span::Span; use crate::types::PhpType; builtin! { @@ -23,6 +22,7 @@ builtin! { params: [ref array: Mixed, callback: Mixed], returns: Void, check: check, + lazy_check: true, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Uksort, ), @@ -32,19 +32,20 @@ builtin! { /// Validates the array and comparator callback arguments for a `uksort` call. /// -/// `uksort` compares array keys, which are always integers in the supported subset. -/// The comparator is validated with two integer literal dummy arguments. Arity -/// (exactly 2) is pre-validated by the registry. Returns `Ok(PhpType::Void)`. +/// `uksort` compares array KEYS, so both comparator parameters are typed from the array's +/// key type: `Int` for an indexed array, the declared key type for an associative one. An +/// unannotated closure parameter inherits that type; explicit declarations stay +/// authoritative. Arity (exactly 2) is pre-validated by the registry. +/// Returns `Ok(PhpType::Void)`. fn check(cx: &mut BuiltinCheckCtx) -> Result { - cx.checker.infer_type(&cx.args[0], cx.env)?; - cx.checker.infer_type(&cx.args[1], cx.env)?; - let cmp_arg = Expr::new(ExprKind::IntLiteral(0), Span::dummy()); - let dummy_args = vec![cmp_arg.clone(), cmp_arg]; + let arr_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let key_ty = crate::types::checker::builtins::array_key_type(&arr_ty); let label = format!("{}() callback", cx.name); - crate::types::checker::builtins::check_callback_builtin_call( + let callback_arg_types = [key_ty.clone(), key_ty]; + crate::types::checker::builtins::check_array_callback_builtin_call( cx.checker, &cx.args[1], - &dummy_args, + &callback_arg_types, cx.span, cx.env, &label, diff --git a/src/builtins/callables/__elephc_object_is_enum.rs b/src/builtins/callables/__elephc_object_is_enum.rs new file mode 100644 index 0000000000..2d64f4b0c7 --- /dev/null +++ b/src/builtins/callables/__elephc_object_is_enum.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Home of the internal `__elephc_object_is_enum` builtin: reports whether a +//! runtime value is a PHP enum case rather than an ordinary object. +//! +//! Called from: +//! - The injected `var_export` prelude (`src/var_export_prelude.rs`), which +//! renders an enum case as `\Enum::Case` instead of `__set_state(...)`. +//! +//! Key details: +//! - `internal: true`: never PHP-visible, so `--strict-php` cannot hide it from +//! the prelude and no user program can call it. +//! - There is no PHP-visible equivalent to alias. `$v instanceof UnitEnum` is the +//! PHP spelling, but elephc does not yet report enum cases as implementing +//! `UnitEnum`, and `enum_exists()` requires a string LITERAL in AOT mode — so a +//! prelude that only ever sees a runtime `mixed` has no other way to ask. +//! - Answers from the class id in the object header via `_class_enum_kinds`, so it +//! is a bounds-checked table load with no allocation and no class-name compare. + +builtin! { + name: "__elephc_object_is_enum", + area: Callables, + params: [value: Mixed], + returns: Bool, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ElephcObjectIsEnum, + ), + summary: "Internal: reports whether a value is a PHP enum case.", + internal: true, +} diff --git a/src/builtins/callables/__elephc_object_prop_count.rs b/src/builtins/callables/__elephc_object_prop_count.rs new file mode 100644 index 0000000000..46ff61fb07 --- /dev/null +++ b/src/builtins/callables/__elephc_object_prop_count.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the internal `__elephc_object_prop_count` builtin: the number of +//! properties an object renders, i.e. the row count of its display descriptor. +//! +//! Called from: +//! - The injected `var_export` prelude (`src/var_export_prelude.rs`), as the bound +//! of the loop that walks an object's properties. +//! +//! Key details: +//! - `internal: true`: never PHP-visible. PHP's `get_object_vars()` is the closest +//! equivalent, but it returns an array of `mixed` and elephc has no +//! object-to-array conversion; a count plus per-index accessors keeps the prelude +//! in ordinary PHP control flow with no new container type. +//! - Reads `_class_prop_desc_ptrs[class_id]`, the SAME rows `var_dump` and +//! `print_r` walk, so the three renderers cannot disagree about which properties +//! an object has. A non-object value reports 0. + +builtin! { + name: "__elephc_object_prop_count", + area: Callables, + params: [value: Mixed], + returns: Int, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ElephcObjectPropCount, + ), + summary: "Internal: number of renderable properties on an object.", + internal: true, +} diff --git a/src/builtins/callables/__elephc_object_prop_name.rs b/src/builtins/callables/__elephc_object_prop_name.rs new file mode 100644 index 0000000000..3e83c7d295 --- /dev/null +++ b/src/builtins/callables/__elephc_object_prop_name.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the internal `__elephc_object_prop_name` builtin: the bare name of an +//! object's Nth renderable property. +//! +//! Called from: +//! - The injected `var_export` prelude (`src/var_export_prelude.rs`), which quotes +//! the name as the `'key' =>` part of a `__set_state(...)` / `(object) array(...)` +//! entry. +//! +//! Key details: +//! - `internal: true`: never PHP-visible. +//! - Returns the BARE property name — `var_export` never annotates visibility, +//! unlike `print_r`'s `x:protected`. Both spellings live in the same descriptor +//! row, so they are one edit apart and cannot drift. +//! - Returns the EMPTY string for an out-of-range index, a non-object value, or a +//! typed property that is still uninitialized. PHP omits uninitialized typed +//! properties from `var_export` output, and a real property name is never empty, +//! so the prelude simply skips empty names. + +builtin! { + name: "__elephc_object_prop_name", + area: Callables, + params: [value: Mixed, index: Int], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ElephcObjectPropName, + ), + summary: "Internal: bare name of an object's Nth renderable property.", + internal: true, +} diff --git a/src/builtins/callables/__elephc_object_prop_value.rs b/src/builtins/callables/__elephc_object_prop_value.rs new file mode 100644 index 0000000000..c255bed483 --- /dev/null +++ b/src/builtins/callables/__elephc_object_prop_value.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the internal `__elephc_object_prop_value` builtin: the value held by an +//! object's Nth renderable property, boxed as a `Mixed` cell. +//! +//! Called from: +//! - The injected `var_export` prelude (`src/var_export_prelude.rs`), which feeds +//! the value straight back into its recursive renderer. +//! +//! Key details: +//! - `internal: true`: never PHP-visible. +//! - OWNERSHIP: the result is `Fresh`. Every property slot is re-boxed through +//! `__rt_mixed_from_value`, which persists a string payload and increfs a +//! container/object payload, so the returned cell is independently owned and the +//! caller's ordinary release cannot damage the object it came from. Handing back +//! a property's own `Mixed` cell instead would alias object storage into a +//! caller-released temporary. +//! - A missing index, a non-object value, an uninitialized typed property, or a +//! slot holding the in-band null sentinel all box canonical PHP `null`. + +builtin! { + name: "__elephc_object_prop_value", + area: Callables, + params: [value: Mixed, index: Int], + returns: Mixed, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ElephcObjectPropValue, + ), + summary: "Internal: value of an object's Nth renderable property.", + internal: true, +} diff --git a/src/builtins/callables/mod.rs b/src/builtins/callables/mod.rs index 1dc5a516b6..6c5fb9c04b 100644 --- a/src/builtins/callables/mod.rs +++ b/src/builtins/callables/mod.rs @@ -49,3 +49,16 @@ pub mod function_exists; pub mod method_exists; pub mod preg_replace_callback; pub mod property_exists; + +// Internal object-introspection aliases used by the injected `var_export` +// prelude. They have no PHP-visible counterpart to alias: PHP would use +// `get_object_vars()` / `$v instanceof UnitEnum`, neither of which elephc can +// express for a runtime `mixed` today. +#[allow(non_snake_case)] +pub mod __elephc_object_is_enum; +#[allow(non_snake_case)] +pub mod __elephc_object_prop_count; +#[allow(non_snake_case)] +pub mod __elephc_object_prop_name; +#[allow(non_snake_case)] +pub mod __elephc_object_prop_value; diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs index f2b39bc3e8..4199ba2946 100644 --- a/src/builtins/docs.rs +++ b/src/builtins/docs.rs @@ -145,6 +145,8 @@ fn semantics_json(semantics: BuiltinSemantics) -> Value { BuiltinArgumentLowering::PregReplaceCallback => "preg_replace_callback", BuiltinArgumentLowering::PositionalRegex => "positional_regex", BuiltinArgumentLowering::UserValueSort => "user_value_sort", + BuiltinArgumentLowering::ArraySplice => "array_splice", + BuiltinArgumentLowering::ArrayInternalPointer(_) => "array_internal_pointer", }; let callable = match semantics.callable { BuiltinCallablePolicy::Dynamic(_) => json!({"kind": "dynamic"}), diff --git a/src/builtins/io/file.rs b/src/builtins/io/file.rs index 3fbca98304..3649d59455 100644 --- a/src/builtins/io/file.rs +++ b/src/builtins/io/file.rs @@ -5,18 +5,24 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: +//! - PHP's signature is `file(string $filename, int $flags = 0, $context = null)`; elephc declares +//! `filename` and `flags`. The stream-context parameter is not modelled, so it is left out +//! rather than accepted and ignored. +//! - `flags` is an ordinary run-time integer bitmask (`FILE_USE_INCLUDE_PATH`, +//! `FILE_IGNORE_NEW_LINES`, `FILE_SKIP_EMPTY_LINES`), NOT a shape-changing literal: the result +//! is `Array` for every flag combination, so it does not need to be known at compile time. //! - `check` returns `Array` (the file's lines). A check hook is required //! because the array return type cannot be expressed through the scalar `returns:` //! field. -use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; use crate::types::PhpType; builtin! { name: "file", area: Io, - params: [filename: Str], + params: [filename: Str, flags: Int = DefaultSpec::Int(0)], returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -27,6 +33,11 @@ builtin! { } /// Returns `Array` reflecting that `file` yields the file's lines as strings. +/// +/// The `$flags` bitmask only changes the CONTENT of the lines (trailing newline removal and +/// empty-line skipping), never the container shape, so the result type is flag-independent. +/// Arity (1 or 2) is pre-validated by the registry, and the registry already inferred every +/// argument once for side effects. fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_type(&cx.args[0], cx.env)?; Ok(PhpType::Array(Box::new(PhpType::Str))) diff --git a/src/builtins/io/file_get_contents.rs b/src/builtins/io/file_get_contents.rs index cdbc99be9b..368fb6792a 100644 --- a/src/builtins/io/file_get_contents.rs +++ b/src/builtins/io/file_get_contents.rs @@ -14,15 +14,27 @@ //! `ftps://` URL links `elephc_tls`; a non-literal path conservatively links //! `elephc_tls`, `elephc_phar`, `z`, and `bz2` because the scheme and PHAR entry //! flags are unknown until run time. +//! - The signature matches reference PHP 8.4 exactly: +//! `file_get_contents(string $filename, bool $use_include_path = false, +//! ?resource $context = null, int $offset = 0, ?int $length = null)`. +//! `$context` has no `TypeSpec` for `resource`, so it is declared `Mixed` with a +//! `null` default exactly like `fopen()`'s `$context`; the backend rejects a +//! non-null one instead of ignoring it. -use crate::builtins::spec::BuiltinCheckCtx; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; use crate::errors::CompileError; use crate::types::PhpType; builtin! { name: "file_get_contents", area: Io, - params: [filename: Str], + params: [ + filename: Str, + use_include_path: Bool = DefaultSpec::Bool(false), + context: Mixed = DefaultSpec::Null, + offset: Int = DefaultSpec::Int(0), + length: Mixed = DefaultSpec::Null + ], returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( diff --git a/src/builtins/math/abs.rs b/src/builtins/math/abs.rs index dd90ef5c41..7aef3e9ed2 100644 --- a/src/builtins/math/abs.rs +++ b/src/builtins/math/abs.rs @@ -8,9 +8,15 @@ //! - A `check` hook is required because the return type depends on the argument type: //! `Float` input returns `Float`, `Mixed`/Union-containing-Float returns `Mixed`, //! and all other inputs return `Int`. +//! - A runtime `Int` argument returns `Mixed`, not `Int`: `abs(PHP_INT_MIN)` has no `int` +//! representation and PHP promotes it to `float(9.2233720368547758E+18)`. This mirrors how +//! `$a + $b` on two runtime ints is already typed `Mixed` so the checked helper can promote +//! on overflow. An `int` *literal* argument is still exact, so it keeps the precise `Int` +//! (or `Mixed` for the single overflowing literal) result. use crate::builtins::spec::BuiltinCheckCtx; use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; use crate::types::PhpType; builtin! { @@ -38,6 +44,19 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { PhpType::Union(ref members) if members.iter().any(|m| *m == PhpType::Mixed) => { PhpType::Mixed } + PhpType::Int => int_abs_result_type(&cx.args[0]), _ => PhpType::Int, }) } + +/// Returns the result type of `abs()` applied to an `int`-typed argument expression. +/// +/// A literal is exact: every value except `PHP_INT_MIN` has an `int` absolute value, and the +/// one that does not still needs the boxed `Mixed` result so the backend can hand back the +/// promoted float. A runtime `int` could be `PHP_INT_MIN`, so it must stay `Mixed`. +fn int_abs_result_type(arg: &Expr) -> PhpType { + match &arg.kind { + ExprKind::IntLiteral(value) if value.checked_abs().is_some() => PhpType::Int, + _ => PhpType::Mixed, + } +} diff --git a/src/builtins/math/base_convert.rs b/src/builtins/math/base_convert.rs new file mode 100644 index 0000000000..8a7dd4168e --- /dev/null +++ b/src/builtins/math/base_convert.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Home of the PHP `base_convert` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - PHP names the first parameter `$num`, not `$number`; named-argument calls depend on it. +//! - Both base arguments raise php-src's `ValueError` outside `2..=36`, so the runtime +//! function is declared `MAY_THROW` and cannot be eliminated as dead code. +//! - Values past `PHP_INT_MAX` widen to `double` during the parse and render through +//! php-src's lossy float loop; `crate::codegen_support::runtime::strings::base_convert` +//! owns that contract. + +builtin! { + name: "base_convert", + area: Math, + params: [num: Str, from_base: Int, to_base: Int], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::BaseConvert, + ), + summary: "Converts a number between two arbitrary bases from 2 to 36.", + php_manual: "https://www.php.net/manual/en/function.base-convert.php", +} diff --git a/src/builtins/math/bindec.rs b/src/builtins/math/bindec.rs new file mode 100644 index 0000000000..034d0a9dad --- /dev/null +++ b/src/builtins/math/bindec.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `bindec` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `bindec(string $binary_string): int|float`. The union return +//! type cannot be spelled in the `builtin!` `returns:` field, so the declared type is +//! `Mixed` (the shared codegen representation of a union) and the `check` hook supplies the +//! precise `int|float` contract. +//! - Characters that are not binary digits are IGNORED rather than ending the scan, and the +//! result widens to `float` once it would exceed `PHP_INT_MAX`; both behaviours live in the +//! shared `__rt_base_to_number` runtime helper. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "bindec", + area: Math, + params: [binary_string: Str], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Bindec, + ), + summary: "Converts a binary string to its decimal number.", + php_manual: "https://www.php.net/manual/en/function.bindec.php", +} + +/// Returns `PhpType::Union([Int, Float])` for a `bindec` call. +/// +/// The `builtin!` macro cannot express a union return type inline, so the precise +/// `int|float` contract is supplied here. A value that fits `PHP_INT_MAX` is an `int`; +/// anything larger widens to `float`, which is only decidable at runtime. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Float])) +} diff --git a/src/builtins/math/decbin.rs b/src/builtins/math/decbin.rs new file mode 100644 index 0000000000..5b76e79d74 --- /dev/null +++ b/src/builtins/math/decbin.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Home of the PHP `decbin` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `decbin(int $num): string`. +//! - The value is rendered as an UNSIGNED 64-bit quantity, so `decbin(-1)` is 64 `1` digits; +//! the shared `__rt_dec_to_base` renderer owns that contract. + +builtin! { + name: "decbin", + area: Math, + params: [num: Int], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Decbin, + ), + summary: "Converts an integer to its binary string representation.", + php_manual: "https://www.php.net/manual/en/function.decbin.php", +} diff --git a/src/builtins/math/dechex.rs b/src/builtins/math/dechex.rs new file mode 100644 index 0000000000..a069bc10a9 --- /dev/null +++ b/src/builtins/math/dechex.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Home of the PHP `dechex` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `dechex(int $num): string`. +//! - The value is rendered as an UNSIGNED 64-bit quantity, so `dechex(-1)` is +//! `"ffffffffffffffff"`; the shared `__rt_dec_to_base` renderer owns that contract. +//! - Digits above 9 use lowercase letters, exactly like reference PHP. + +builtin! { + name: "dechex", + area: Math, + params: [num: Int], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Dechex, + ), + summary: "Converts an integer to its hexadecimal string representation.", + php_manual: "https://www.php.net/manual/en/function.dechex.php", +} diff --git a/src/builtins/math/decoct.rs b/src/builtins/math/decoct.rs new file mode 100644 index 0000000000..7848ec1ff8 --- /dev/null +++ b/src/builtins/math/decoct.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Home of the PHP `decoct` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `decoct(int $num): string`. +//! - The value is rendered as an UNSIGNED 64-bit quantity, so `decoct(-1)` is +//! `"1777777777777777777777"`; the shared `__rt_dec_to_base` renderer owns that contract. + +builtin! { + name: "decoct", + area: Math, + params: [num: Int], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Decoct, + ), + summary: "Converts an integer to its octal string representation.", + php_manual: "https://www.php.net/manual/en/function.decoct.php", +} diff --git a/src/builtins/math/hexdec.rs b/src/builtins/math/hexdec.rs new file mode 100644 index 0000000000..4e9afffa58 --- /dev/null +++ b/src/builtins/math/hexdec.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `hexdec` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `hexdec(string $hex_string): int|float`. The union return +//! type cannot be spelled in the `builtin!` `returns:` field, so the declared type is +//! `Mixed` (the shared codegen representation of a union) and the `check` hook supplies the +//! precise `int|float` contract. +//! - Characters that are not hexadecimal digits are IGNORED rather than ending the scan, and the +//! result widens to `float` once it would exceed `PHP_INT_MAX`; both behaviours live in the +//! shared `__rt_base_to_number` runtime helper. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "hexdec", + area: Math, + params: [hex_string: Str], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Hexdec, + ), + summary: "Converts a hexadecimal string to its decimal number.", + php_manual: "https://www.php.net/manual/en/function.hexdec.php", +} + +/// Returns `PhpType::Union([Int, Float])` for a `hexdec` call. +/// +/// The `builtin!` macro cannot express a union return type inline, so the precise +/// `int|float` contract is supplied here. A value that fits `PHP_INT_MAX` is an `int`; +/// anything larger widens to `float`, which is only decidable at runtime. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Float])) +} diff --git a/src/builtins/math/max.rs b/src/builtins/math/max.rs index 18ecd08ce5..50f8f5687f 100644 --- a/src/builtins/math/max.rs +++ b/src/builtins/math/max.rs @@ -6,8 +6,10 @@ //! //! Key details: //! - A `check` hook is required because the return type depends on argument types: -//! any Float argument widens the result to Float; otherwise the result is Int. -//! - `min_args: 2` enforces the legacy requirement that at least two values be provided. +//! the single-array form returns the array's element type, while the variadic +//! form widens to Float as soon as any argument is Float. +//! - `min_args: 1` matches PHP: `max()` with no argument is an ArgumentCountError, +//! one argument must be an array, and two or more arguments compare the values. use crate::builtins::spec::BuiltinCheckCtx; use crate::errors::CompileError; @@ -18,8 +20,8 @@ builtin! { area: Math, params: [value: Mixed], variadic: "values", - min_args: 2, - arity_error: "max() requires at least 2 arguments", + min_args: 1, + arity_error: "max() expects at least 1 argument, 0 given", returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -29,8 +31,12 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.max.php", } -/// Returns Float when any argument is Float, otherwise returns Int. +/// Returns the array element type for the single-array form; otherwise returns Float +/// when any argument is Float and Int in every other case. fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() == 1 { + return super::min_max_array_element_type(cx, "max"); + } let mut has_float = false; for arg in cx.args { let t = cx.checker.infer_type(arg, cx.env)?; diff --git a/src/builtins/math/min.rs b/src/builtins/math/min.rs index ca3a3b6945..768aa66469 100644 --- a/src/builtins/math/min.rs +++ b/src/builtins/math/min.rs @@ -6,8 +6,10 @@ //! //! Key details: //! - A `check` hook is required because the return type depends on argument types: -//! any Float argument widens the result to Float; otherwise the result is Int. -//! - `min_args: 2` enforces the legacy requirement that at least two values be provided. +//! the single-array form returns the array's element type, while the variadic +//! form widens to Float as soon as any argument is Float. +//! - `min_args: 1` matches PHP: `min()` with no argument is an ArgumentCountError, +//! one argument must be an array, and two or more arguments compare the values. use crate::builtins::spec::BuiltinCheckCtx; use crate::errors::CompileError; @@ -18,8 +20,8 @@ builtin! { area: Math, params: [value: Mixed], variadic: "values", - min_args: 2, - arity_error: "min() requires at least 2 arguments", + min_args: 1, + arity_error: "min() expects at least 1 argument, 0 given", returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -29,8 +31,12 @@ builtin! { php_manual: "https://www.php.net/manual/en/function.min.php", } -/// Returns Float when any argument is Float, otherwise returns Int. +/// Returns the array element type for the single-array form; otherwise returns Float +/// when any argument is Float and Int in every other case. fn check(cx: &mut BuiltinCheckCtx) -> Result { + if cx.args.len() == 1 { + return super::min_max_array_element_type(cx, "min"); + } let mut has_float = false; for arg in cx.args { let t = cx.checker.infer_type(arg, cx.env)?; diff --git a/src/builtins/math/mod.rs b/src/builtins/math/mod.rs index 36aaa06857..7d37ccd890 100644 --- a/src/builtins/math/mod.rs +++ b/src/builtins/math/mod.rs @@ -13,21 +13,33 @@ //! `returns` type. //! - Builtins with argument-type-dependent returns (`abs`, `clamp`, `min`, `max`) //! supply a `check` hook that computes the precise return type. +//! - `min_max_array_element_type()` is the one shared helper here: `min` and `max` +//! accept the same single-array form, so both home files delegate to it. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; pub mod abs; pub mod acos; pub mod asin; pub mod atan; pub mod atan2; +pub mod base_convert; +pub mod bindec; pub mod ceil; pub mod clamp; pub mod cos; pub mod cosh; +pub mod decbin; +pub mod dechex; +pub mod decoct; pub mod deg2rad; pub mod exp; pub mod fdiv; pub mod floor; pub mod fmod; +pub mod hexdec; pub mod hypot; pub mod intdiv; pub mod log; @@ -36,6 +48,7 @@ pub mod log2; pub mod max; pub mod min; pub mod mt_rand; +pub mod octdec; pub mod pi; pub mod pow; pub mod rad2deg; @@ -47,3 +60,27 @@ pub mod sinh; pub mod sqrt; pub mod tan; pub mod tanh; + +/// Resolves the result type of the single-argument `min()` / `max()` form. +/// +/// PHP's one-argument form takes an array and returns one of its elements, so the +/// call's result type is the array's element type. A non-array argument is PHP's +/// `min(): Argument #1 ($value) must be of type array, given` TypeError; +/// elephc reports it at compile time because the argument type is already known. +pub(crate) fn min_max_array_element_type( + cx: &mut BuiltinCheckCtx, + name: &str, +) -> Result { + let ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + match ty { + PhpType::Array(element) => Ok(*element), + PhpType::AssocArray { value, .. } => Ok(*value), + other => Err(CompileError::new( + cx.span, + &format!( + "{}(): Argument #1 ($value) must be of type array, {} given", + name, other + ), + )), + } +} diff --git a/src/builtins/math/octdec.rs b/src/builtins/math/octdec.rs new file mode 100644 index 0000000000..2000b7b024 --- /dev/null +++ b/src/builtins/math/octdec.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Home of the PHP `octdec` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `octdec(string $octal_string): int|float`. The union return +//! type cannot be spelled in the `builtin!` `returns:` field, so the declared type is +//! `Mixed` (the shared codegen representation of a union) and the `check` hook supplies the +//! precise `int|float` contract. +//! - Characters that are not octal digits are IGNORED rather than ending the scan, and the +//! result widens to `float` once it would exceed `PHP_INT_MAX`; both behaviours live in the +//! shared `__rt_base_to_number` runtime helper. + +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "octdec", + area: Math, + params: [octal_string: Str], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Octdec, + ), + summary: "Converts a octal string to its decimal number.", + php_manual: "https://www.php.net/manual/en/function.octdec.php", +} + +/// Returns `PhpType::Union([Int, Float])` for a `octdec` call. +/// +/// The `builtin!` macro cannot express a union return type inline, so the precise +/// `int|float` contract is supplied here. A value that fits `PHP_INT_MAX` is an `int`; +/// anything larger widens to `float`, which is only decidable at runtime. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::Float])) +} diff --git a/src/builtins/math/round.rs b/src/builtins/math/round.rs index c31b44fc10..98e0c2a93b 100644 --- a/src/builtins/math/round.rs +++ b/src/builtins/math/round.rs @@ -8,15 +8,24 @@ //! Key details: //! - No `check` hook is needed: `round` is a pure-data builtin whose return type //! (`Float`) is fully determined by its declaration. -//! - The second parameter `precision` is optional with a default of `0`, matching -//! PHP's `round(num, precision = 0)` signature. The registry enforces 1-2 args. +//! - The second parameter `precision` and the third parameter `mode` are optional with +//! defaults of `0` and `PHP_ROUND_HALF_UP` (`1`), matching PHP 8.4's +//! `round(num, precision = 0, mode = RoundingMode::HalfAwayFromZero)` signature. The +//! registry enforces 1-3 args. +//! - `$mode` is NOT validated here: PHP raises a catchable `ValueError` at runtime for an +//! out-of-range mode, and the mode can be a runtime value, so the guard lives in the +//! backend (`codegen::lower_inst::builtins::round_mode`) next to the ABI materialization. use crate::builtins::spec::DefaultSpec; builtin! { name: "round", area: Math, - params: [num: Float, precision: Int = DefaultSpec::Int(0)], + params: [ + num: Float, + precision: Int = DefaultSpec::Int(0), + mode: Int = DefaultSpec::Int(1) + ], returns: Float, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Round, diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 4fd6fc5fc2..02093d457d 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -794,11 +794,14 @@ mod tests { } /// Verifies typed runtime lowering derives count's visible arity from registry semantics. + /// + /// `count()` now declares PHP's optional `$mode`, so the bounds are `1..=2`; the backend + /// still accepts a single operand because a statically-zero mode is pruned during lowering. #[test] fn count_runtime_function_arity_comes_from_registry_semantics() { assert_eq!( runtime_fn_arity_bounds(crate::ir::RuntimeFnId::Count), - Some((1, Some(1))), + Some((1, Some(2))), ); } diff --git a/src/builtins/semantics.rs b/src/builtins/semantics.rs index cefe5592d4..60a61fa54b 100644 --- a/src/builtins/semantics.rs +++ b/src/builtins/semantics.rs @@ -161,6 +161,63 @@ pub enum BuiltinArgumentLowering { PositionalRegex, /// Preserve by-reference array storage while lowering user-comparator sorts. UserValueSort, + /// Promote a typed `array_splice()` receiver whose `$replacement` changes the element type. + /// + /// PHP's `array_splice($a, 1, 1, ["x"])` on `$a = [1, 2, 3]` leaves a heterogeneous + /// `[1, "x", 3]`. elephc types an indexed array at its payload slot, so that promotion has + /// to happen to the receiver LOCAL before the call: the slot widens to `array` and + /// `__rt_array_to_mixed` re-boxes the live payloads. Only the argument lowering can see both + /// the receiver and the replacement, which is why it is a registry-owned strategy rather + /// than a per-argument rule. + ArraySplice, + /// Bind the receiver to its hidden internal-array-pointer cursor slot. + /// + /// PHP's `key`/`current`/`next`/`prev`/`reset`/`end` read and move a per-array + /// cursor that has no place in elephc's array headers, so lowering resolves the + /// receiver to a plain local and pairs it with a compiler-allocated cursor slot + /// instead of passing the array alone. + ArrayInternalPointer(ArrayPointerOp), +} + +/// One PHP internal-array-pointer operation, selected by the registry declaration. +/// +/// Lowering reads this instead of matching on the PHP function name, so the six +/// builtins stay distinguishable through typed metadata all the way to EIR. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ArrayPointerOp { + /// `key($array)`: box the key under the cursor, or null when it is invalid. + Key, + /// `current($array)`: box the value under the cursor, or false when it is invalid. + Current, + /// `next(&$array)`: advance the cursor one position, then read it. + Next, + /// `prev(&$array)`: rewind the cursor one position, then read it. + Prev, + /// `reset(&$array)`: move the cursor to the first element, then read it. + Reset, + /// `end(&$array)`: move the cursor to the last element, then read it. + End, +} + +impl ArrayPointerOp { + /// Returns the seek mode consumed by `__rt_array_ptr_seek`, or `None` for pure reads. + /// + /// `Key` and `Current` never move the cursor, so they have no seek mode; the other + /// four map onto the `ARRAY_PTR_SEEK_*` constants owned by the runtime emitter. + pub const fn seek_mode(self) -> Option { + match self { + ArrayPointerOp::Key | ArrayPointerOp::Current => None, + ArrayPointerOp::Reset => Some(0), + ArrayPointerOp::End => Some(1), + ArrayPointerOp::Next => Some(2), + ArrayPointerOp::Prev => Some(3), + } + } + + /// Returns whether the operation reports the cursor's key rather than its value. + pub const fn reads_key(self) -> bool { + matches!(self, ArrayPointerOp::Key) + } } impl BuiltinRuntimeFunctions { @@ -377,6 +434,41 @@ pub const fn runtime_fn_semantics(target: RuntimeFnId) -> BuiltinSemantics { } } +/// Builds the complete shared descriptor for one PHP internal-array-pointer builtin. +/// +/// The six pointer builtins are lowered as a unit by +/// `BuiltinArgumentLowering::ArrayInternalPointer`, which resolves the receiver to a +/// plain local, pairs it with that local's hidden cursor slot, and emits the typed +/// `ArrayPtrSeek` / `ArrayPtrKey` / `ArrayPtrValue` runtime calls itself. Two +/// consequences are encoded here: +/// +/// - `runtime_functions` stays `None`. The declared arity is one PHP argument, but the +/// emitted runtime calls carry two or three operands; publishing an inventory entry +/// would bind that one-argument arity onto the runtime signature and fail EIR +/// validation. +/// - `callable` is `StaticOnly`. A runtime-selected name has no receiver expression, so +/// there is no local to attach a cursor to. +pub const fn array_pointer_semantics( + op: ArrayPointerOp, + target: RuntimeFnId, +) -> BuiltinSemantics { + BuiltinSemantics { + validation: BuiltinValidation::SignatureOnly, + result_type: BuiltinResultType::Declared, + effects: BuiltinEffects::Static(target.effects()), + result_ownership: BuiltinResultOwnership::Fresh, + requirements: BuiltinRequirements::Static(&[]), + target_strategy: BuiltinTargetStrategy::RuntimeCall, + target_support: BuiltinTargetSupport::All, + runtime_functions: BuiltinRuntimeFunctions::None, + argument_lowering: BuiltinArgumentLowering::ArrayInternalPointer(op), + callable: BuiltinCallablePolicy::StaticOnly( + "the internal array pointer needs a named array variable receiver", + ), + lowering: BuiltinLowering::Runtime(RuntimeCallTarget::Function(target)), + } +} + /// Builds shared semantics for a PHP runtime-type predicate. pub const fn type_predicate_semantics(predicate: PhpTypePredicate) -> BuiltinSemantics { BuiltinSemantics { diff --git a/src/builtins/string/base64_decode.rs b/src/builtins/string/base64_decode.rs index 08a0ea34d3..925fdeb6dc 100644 --- a/src/builtins/string/base64_decode.rs +++ b/src/builtins/string/base64_decode.rs @@ -5,20 +5,46 @@ //! - The builtin registry, checker, optimizer, and AST-to-EIR builtin lowering path. //! //! Key details: -//! - The typed runtime target has a validated `Str -> Str` EIR signature. -//! - Concrete helper symbols and registers are selected only by the target backend. +//! - The declared signature is PHP's own +//! `base64_decode(string $string, bool $strict = false): string|false`. The `$strict` flag +//! is what makes the result a union: reference PHP returns `false` from a strict decode +//! whose input holds a character outside the Base64 alphabet, a misplaced `=`, or a +//! truncated final group. +//! - `check` returns `string|false`, whose codegen representation is `Mixed`, so the backend +//! hands back a BOXED cell (the `strstr()` / `phpversion($ext)` shape) instead of a raw +//! string-register pair. +//! - Decoding itself follows php-src's `php_base64_decode_impl`: a per-character reverse +//! table where whitespace is skipped in both modes, an `i % 4` accumulator that does not +//! restart on skipped bytes, and PHP's padding rules. `_b64_decode_tbl` carries the +//! sentinels that distinguish "skip" from "reject". -use crate::ir::{RuntimeCallTarget, UnaryStringRuntime}; +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::types::PhpType; builtin! { name: "base64_decode", area: String, - params: [string: Str], - returns: Str, - semantics: crate::builtins::semantics::unary_string_runtime( - RuntimeCallTarget::UnaryString(UnaryStringRuntime::Base64Decode), - crate::ir::Effects::PURE, + params: [string: Str, strict: Bool = DefaultSpec::Bool(false)], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Base64Decode, ), summary: "Decodes a Base64-encoded string back into its original data.", php_manual: "https://www.php.net/manual/en/function.base64-decode.php", } + +/// Returns `string|false` for every `base64_decode()` call. +/// +/// ONE type for every arity, deliberately. A one-argument (lax) call can never fail, but the +/// checker-facing type and the backend's storage layout are a single shared contract: the +/// lowering always boxes its answer into a `Mixed` cell because the two-argument form may +/// yield `false`, and narrowing the lax arity back to `Str` here would make `store_if_result` +/// copy the string-pair registers that no longer hold the answer. `strstr()` documents the +/// same reasoning and the miscompile that disagreement causes. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + Ok(cx + .checker + .normalize_union_type(vec![PhpType::Str, PhpType::False])) +} diff --git a/src/builtins/string/chunk_split.rs b/src/builtins/string/chunk_split.rs new file mode 100644 index 0000000000..4df79ca874 --- /dev/null +++ b/src/builtins/string/chunk_split.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Home of the PHP `chunk_split` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Accepts a required `string` param plus optional `length` and `separator` params +//! with PHP's `76` / `"\r\n"` defaults. +//! - `$length < 1` raises php-src's `ValueError`, so the runtime function is declared +//! `MAY_THROW` rather than pure and cannot be eliminated as dead code. + +use crate::builtins::spec::DefaultSpec; + +builtin! { + name: "chunk_split", + area: String, + params: [ + string: Str, + length: Int = DefaultSpec::Int(76), + separator: Str = DefaultSpec::Str("\r\n") + ], + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::ChunkSplit, + ), + summary: "Splits a string into fixed-length chunks separated by a given string.", + php_manual: "https://www.php.net/manual/en/function.chunk-split.php", +} diff --git a/src/builtins/string/count_chars.rs b/src/builtins/string/count_chars.rs new file mode 100644 index 0000000000..4a828060d7 --- /dev/null +++ b/src/builtins/string/count_chars.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Home of the PHP `count_chars` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - php-src's return type is `array|string`, chosen by the `$mode` VALUE: modes `0`, `1`, +//! and `2` build a byte-value keyed tally, while modes `3` and `4` render the used or +//! unused bytes as a string. elephc compiles ahead of time, so `$mode` has to be an +//! integer literal (constant folding runs before the checker, so a named constant still +//! qualifies). +//! - A mode outside `0..=4` keeps the tally shape here and the backend raises php-src's +//! catchable `ValueError` before the helper runs, so the runtime function is declared +//! `MAY_THROW` rather than pure. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "count_chars", + area: String, + params: [ + string: Str, + mode: Int = DefaultSpec::Int(0) + ], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::CountChars, + ), + summary: "Returns byte-frequency information about a string as a tally or a byte list.", + php_manual: "https://www.php.net/manual/en/function.count-chars.php", +} + +/// Returns the `$mode`-dependent result type for a `count_chars` call. +/// +/// Argument types are inferred by the common registry dispatch path before this hook fires, +/// and arity is pre-validated by the registry. Modes `3` and `4` return `string`; every other +/// literal (including the `0` default and the out-of-range values php-src rejects with a +/// `ValueError`) returns the `array` tally. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let tally = PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Int), + }; + let Some(mode) = mode_argument(cx.args) else { + return Ok(tally); + }; + let ExprKind::IntLiteral(mode) = mode.kind else { + return Err(CompileError::new( + cx.span, + "count_chars() mode argument must be an integer literal in AOT mode", + )); + }; + match mode { + 3 | 4 => Ok(PhpType::Str), + _ => Ok(tally), + } +} + +/// Returns the `$mode` argument expression from a call's source-order argument list. +/// +/// A `mode:` named argument is matched by name first so `count_chars($s, mode: 3)` resolves +/// to the same result type as the positional spelling; otherwise the second positional +/// argument is used, skipping any named argument that occupies that slot. +fn mode_argument(args: &[crate::parser::ast::Expr]) -> Option<&crate::parser::ast::Expr> { + for arg in args { + if let ExprKind::NamedArg { name, value } = &arg.kind { + if name == "mode" { + return Some(value); + } + } + } + args.iter() + .filter(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. })) + .nth(1) +} diff --git a/src/builtins/string/explode.rs b/src/builtins/string/explode.rs index a382117ee1..4572ef7d65 100644 --- a/src/builtins/string/explode.rs +++ b/src/builtins/string/explode.rs @@ -6,8 +6,8 @@ //! //! Key details: //! - The declared signature carries the full golden param list (`separator`, `string`, -//! `limit`), but `max_args: 2` caps `check_arity` so a third argument is rejected, -//! matching the legacy CHECK arm which enforced exactly two arguments. +//! `limit`); `$limit` defaults to `PHP_INT_MAX`, which is how php-src spells "no limit", +//! so `RuntimeFnId::Explode` sees one uniform three-argument contract. //! - `check` returns `PhpType::Array(Box::new(PhpType::Str))`. A check hook is required //! because the `builtin!` macro `returns:` field cannot express an array type inline. //! Argument types are inferred by the common registry dispatch path before the hook @@ -21,7 +21,6 @@ builtin! { name: "explode", area: String, params: [separator: Str, string: Str, limit: Int = DefaultSpec::IntMax], - max_args: 2, returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -35,7 +34,7 @@ builtin! { /// /// A check hook is required because the `builtin!` macro cannot express array return /// types inline. Argument types are inferred by the common registry dispatch path before -/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +/// this hook fires; arity is validated by the registry from the declared parameter list. fn check(_cx: &mut BuiltinCheckCtx) -> Result { Ok(PhpType::Array(Box::new(PhpType::Str))) } diff --git a/src/builtins/string/implode.rs b/src/builtins/string/implode.rs index e49c1d6ec0..c08dd4c9aa 100644 --- a/src/builtins/string/implode.rs +++ b/src/builtins/string/implode.rs @@ -5,45 +5,28 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - `implode` is the one builtin whose supported compiler contract (exactly 2 arguments) -//! is stricter than its golden signature's minimum. The golden marks `array` optional -//! (required count 1), which the parity gate compares against, so `array` must keep a -//! default here. `max_args` caps only the maximum, so it cannot raise the minimum; -//! the exact-2 requirement is therefore re-enforced inside the `check` hook to keep the -//! established `"implode() takes exactly 2 arguments"` diagnostic for the tested 1-arg call. -//! - `check` returns `PhpType::Str`. +//! - Reference PHP declares `implode(string|array $separator, ?array $array = null)`, which is +//! what makes BOTH accepted call forms work: `implode($separator, $array)` and the +//! single-argument `implode($array)` that joins with an empty separator. `separator` is +//! therefore declared `Mixed`, not `Str`, and the backend picks the operand roles from the +//! argument count (`lower_implode` in `crate::codegen::lower_inst::builtins::strings`). +//! - The legacy reversed `implode($array, $separator)` order was REMOVED in PHP 8.0 and is +//! deliberately not accepted. +//! - This declaration mirrors the `join` alias exactly; the registry's alias arity gate requires +//! the two to agree on their enforced bounds (one required parameter, at most two). +//! - No `check` hook narrows the arity: `returns: Str` is authoritative for the checker. -use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; -use crate::errors::CompileError; -use crate::types::PhpType; +use crate::builtins::spec::DefaultSpec; builtin! { name: "implode", area: String, - params: [separator: Str, array: Mixed = DefaultSpec::Null], + params: [separator: Mixed, array: Mixed = DefaultSpec::Null], max_args: 2, returns: Str, - check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Implode, ), summary: "Joins array elements into a single string using a separator.", php_manual: "https://www.php.net/manual/en/function.implode.php", } - -/// Returns `PhpType::Str` for an `implode` call, enforcing the supported exactly-2 arity. -/// -/// The golden signature marks `array` optional (so the parity gate sees one required -/// param), but the compiler contract requires exactly two arguments. `check_arity`'s -/// `max_args` override caps the maximum only and cannot raise the minimum, so the -/// exact-2 requirement is re-enforced here to preserve the established diagnostic. Argument -/// types are inferred by the common registry dispatch path before this hook fires. -fn check(cx: &mut BuiltinCheckCtx) -> Result { - if cx.args.len() != 2 { - return Err(CompileError::new( - cx.span, - "implode() takes exactly 2 arguments", - )); - } - Ok(PhpType::Str) -} diff --git a/src/builtins/string/join.rs b/src/builtins/string/join.rs new file mode 100644 index 0000000000..630fca0fd4 --- /dev/null +++ b/src/builtins/string/join.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Home of the PHP `join` builtin: the registry-visible alias of `implode`. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - `join` shares `implode`'s typed runtime target (`RuntimeFnId::Implode`), so the +//! registry's alias arity gate requires the two declarations to agree on their +//! enforced bounds: one required parameter, at most two. +//! - Reference PHP declares `join(string|array $separator, ?array $array = null)`, which +//! is what makes BOTH accepted call forms work: `join($separator, $array)` and the +//! single-argument `join($array)` that joins with an empty separator. `separator` is +//! therefore declared `Mixed` here, not `Str`, and the backend picks the operand roles +//! from the argument count. (The legacy reversed `join($array, $separator)` order was +//! REMOVED in PHP 8.0 and is deliberately not accepted.) +//! - Unlike `implode`, no `check` hook narrows the arity: the one-argument form is the +//! whole reason this alias carries its own declaration. + +use crate::builtins::spec::DefaultSpec; + +builtin! { + name: "join", + area: String, + params: [separator: Mixed, array: Mixed = DefaultSpec::Null], + max_args: 2, + returns: Str, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Implode, + ), + summary: "Joins array elements into a single string using a separator (alias of implode).", + php_manual: "https://www.php.net/manual/en/function.join.php", +} diff --git a/src/builtins/string/mod.rs b/src/builtins/string/mod.rs index cf7b180a2b..faf3166cea 100644 --- a/src/builtins/string/mod.rs +++ b/src/builtins/string/mod.rs @@ -26,6 +26,8 @@ pub mod base64_encode; pub mod bin2hex; pub mod chop; pub mod chr; +pub mod chunk_split; +pub mod count_chars; pub mod crc32; pub mod ctype_alnum; pub mod ctype_alpha; @@ -49,6 +51,7 @@ pub mod implode; pub mod inet_ntop; pub mod inet_pton; pub mod ip2long; +pub mod join; pub mod lcfirst; pub mod long2ip; pub mod ltrim; @@ -60,6 +63,8 @@ pub mod number_format; pub mod ord; pub mod parse_url; pub mod printf; +pub mod quoted_printable_encode; +pub mod quotemeta; pub mod rawurldecode; pub mod rawurlencode; pub mod rtrim; @@ -74,17 +79,24 @@ pub mod str_repeat; pub mod str_replace; pub mod str_split; pub mod str_starts_with; +pub mod str_word_count; pub mod strcasecmp; pub mod strcmp; pub mod stripslashes; pub mod strlen; +pub mod strncasecmp; +pub mod strncmp; +pub mod stripos; pub mod strpos; pub mod strrev; +pub mod strripos; pub mod strrpos; pub mod strstr; pub mod strtolower; pub mod strtoupper; +pub mod strtr; pub mod substr; +pub mod substr_count; pub mod substr_replace; pub mod trim; pub mod ucfirst; diff --git a/src/builtins/string/quoted_printable_encode.rs b/src/builtins/string/quoted_printable_encode.rs new file mode 100644 index 0000000000..b6992ee77a --- /dev/null +++ b/src/builtins/string/quoted_printable_encode.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Home of the PHP `quoted_printable_encode` builtin and its backend-neutral runtime semantics. +//! +//! Called from: +//! - The builtin registry, checker, optimizer, and AST-to-EIR builtin lowering path. +//! +//! Key details: +//! - The typed runtime target has a validated `Str -> Str` EIR signature. +//! - Encoding only inspects its argument's bytes, so the call is `PURE`. +//! - The MIME quoted-printable rules live in `__rt_quoted_printable_encode`: control bytes, +//! `0x7F`, high-bit bytes, `=`, and a space directly before a `CR` become `=XX`; an embedded +//! `CRLF` is copied through; lines are folded at 75 columns with a trailing `=`. +//! - Concrete helper symbols and registers are selected only by the target backend. + +use crate::ir::{RuntimeCallTarget, UnaryStringRuntime}; + +builtin! { + name: "quoted_printable_encode", + area: String, + params: [string: Str], + returns: Str, + semantics: crate::builtins::semantics::unary_string_runtime( + RuntimeCallTarget::UnaryString(UnaryStringRuntime::QuotedPrintableEncode), + crate::ir::Effects::PURE, + ), + summary: "Encodes a string with the MIME quoted-printable transfer encoding.", + php_manual: "https://www.php.net/manual/en/function.quoted-printable-encode.php", +} diff --git a/src/builtins/string/quotemeta.rs b/src/builtins/string/quotemeta.rs new file mode 100644 index 0000000000..7ebfb6ca9f --- /dev/null +++ b/src/builtins/string/quotemeta.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Home of the PHP `quotemeta` builtin and its backend-neutral runtime semantics. +//! +//! Called from: +//! - The builtin registry, checker, optimizer, and AST-to-EIR builtin lowering path. +//! +//! Key details: +//! - The typed runtime target has a validated `Str -> Str` EIR signature. +//! - Escaping never inspects state outside its argument, so the call is `PURE`. +//! - Concrete helper symbols and registers are selected only by the target backend. + +use crate::ir::{RuntimeCallTarget, UnaryStringRuntime}; + +builtin! { + name: "quotemeta", + area: String, + params: [string: Str], + returns: Str, + semantics: crate::builtins::semantics::unary_string_runtime( + RuntimeCallTarget::UnaryString(UnaryStringRuntime::QuoteMeta), + crate::ir::Effects::PURE, + ), + summary: "Prefixes each regular-expression metacharacter in a string with a backslash.", + php_manual: "https://www.php.net/manual/en/function.quotemeta.php", +} diff --git a/src/builtins/string/str_word_count.rs b/src/builtins/string/str_word_count.rs new file mode 100644 index 0000000000..5ba36eb46f --- /dev/null +++ b/src/builtins/string/str_word_count.rs @@ -0,0 +1,87 @@ +//! Purpose: +//! Home of the PHP `str_word_count` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - php-src's return type is `array|int`, chosen by the `$format` VALUE. elephc is an +//! ahead-of-time compiler, so the result storage must be known at compile time: the +//! `$format` argument therefore has to be an integer literal (constant folding runs +//! before the checker, so `str_word_count($s, MY_FORMAT)` still qualifies). +//! - Format `0` yields `int`, format `1` a list `array`, and format `2` the +//! byte-offset map `array`. Any other literal keeps the `int` shape and the +//! backend raises php-src's catchable `ValueError` before the helper runs, so the runtime +//! function is declared `MAY_THROW` rather than pure. +//! - `$characters` is nullable in php-src; an omitted or empty character list produces the +//! same word mask, so the backend passes a zero-length list for both. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "str_word_count", + area: String, + params: [ + string: Str, + format: Int = DefaultSpec::Int(0), + characters: Str = DefaultSpec::Null + ], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::StrWordCount, + ), + summary: "Counts the words in a string, or returns them as a list or byte-offset map.", + php_manual: "https://www.php.net/manual/en/function.str-word-count.php", +} + +/// Returns the `$format`-dependent result type for a `str_word_count` call. +/// +/// Argument types are inferred by the common registry dispatch path before this hook fires, +/// and arity is pre-validated by the registry. The hook only reads the `$format` argument's +/// literal value: format `1` returns `array`, format `2` returns `array`, +/// and every other literal (including the `0` default and the out-of-range values php-src +/// rejects with a `ValueError`) returns `int`. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let Some(format) = format_argument(cx.args) else { + return Ok(PhpType::Int); + }; + let ExprKind::IntLiteral(format) = format.kind else { + return Err(CompileError::new( + cx.span, + "str_word_count() format argument must be an integer literal in AOT mode", + )); + }; + match format { + 1 => Ok(PhpType::Array(Box::new(PhpType::Str))), + 2 => Ok(PhpType::AssocArray { + key: Box::new(PhpType::Int), + value: Box::new(PhpType::Str), + }), + _ => Ok(PhpType::Int), + } +} + +/// Returns the `$format` argument expression from a call's source-order argument list. +/// +/// A `format:` named argument is matched by name first so `str_word_count($s, format: 1)` +/// resolves to the same result type as the positional spelling; otherwise the second +/// positional argument is used, skipping any named argument that occupies that slot. +fn format_argument(args: &[crate::parser::ast::Expr]) -> Option<&crate::parser::ast::Expr> { + for arg in args { + if let ExprKind::NamedArg { name, value } = &arg.kind { + if name == "format" { + return Some(value); + } + } + } + let positional = args + .iter() + .filter(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. })) + .nth(1)?; + Some(positional) +} diff --git a/src/builtins/string/stripos.rs b/src/builtins/string/stripos.rs new file mode 100644 index 0000000000..de09bef8c6 --- /dev/null +++ b/src/builtins/string/stripos.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Home of the PHP `stripos` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature is PHP's own `stripos(string $haystack, string $needle, int $offset = 0)`, +//! the case-insensitive twin of `strpos()`. `$offset` is accepted positionally and as a named +//! argument; a negative value is resolved against the haystack length, and an offset outside +//! the haystack raises PHP's catchable `ValueError` from the backend lowering — the shared +//! `lower_string_position` path both spellings go through. +//! - Case folding is ASCII-only, matching php-src's locale-independent `zend_tolower_ascii`. +//! - `check` returns `PhpType::Union([Int, False])` (position, or `false` on no match). +//! A check hook is required because the `builtin!` macro `returns:` field only accepts +//! a simple type identifier and cannot express a union inline. Argument types are +//! inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "stripos", + area: String, + params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Stripos, + ), + summary: "Finds the numeric position of the first case-insensitive occurrence of a substring.", + php_manual: "https://www.php.net/manual/en/function.stripos.php", +} + +/// Returns `PhpType::Union([Int, Bool])` for a `stripos` call (position, or `false`). +/// +/// A check hook is required because the `builtin!` macro cannot express a union return +/// type inline. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity is validated by the registry from the declared parameter list. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::False])) +} diff --git a/src/builtins/string/strncasecmp.rs b/src/builtins/string/strncasecmp.rs new file mode 100644 index 0000000000..80ddd61a92 --- /dev/null +++ b/src/builtins/string/strncasecmp.rs @@ -0,0 +1,27 @@ +//! Purpose: +//! Home of the PHP `strncasecmp` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature +//! `strncasecmp(string $string1, string $string2, int $length): int`; all three parameters +//! are required. +//! - Case folding is ASCII-only, exactly like `strcasecmp`, and the result is the raw folded +//! byte difference rather than a clamped `-1/0/1`. +//! - The typed runtime target carries `MAY_THROW`: a negative `$length` raises a catchable +//! `ValueError`, so the call must not be removable by dead-code elimination. + +builtin! { + name: "strncasecmp", + area: String, + params: [string1: Str, string2: Str, length: Int], + returns: Int, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Strncasecmp, + ), + summary: "Compares the first n bytes of two strings, ignoring ASCII case.", + php_manual: "https://www.php.net/manual/en/function.strncasecmp.php", +} diff --git a/src/builtins/string/strncmp.rs b/src/builtins/string/strncmp.rs new file mode 100644 index 0000000000..74a5b4e82e --- /dev/null +++ b/src/builtins/string/strncmp.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Home of the PHP `strncmp` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `strncmp(string $string1, string $string2, int $length): int`; +//! all three parameters are required. +//! - Like `strcmp`, the result is the raw byte difference of the first mismatching pair, not a +//! clamped `-1/0/1`. +//! - The typed runtime target carries `MAY_THROW`: a negative `$length` raises a catchable +//! `ValueError`, so the call must not be removable by dead-code elimination. + +builtin! { + name: "strncmp", + area: String, + params: [string1: Str, string2: Str, length: Int], + returns: Int, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Strncmp, + ), + summary: "Compares the first n bytes of two strings.", + php_manual: "https://www.php.net/manual/en/function.strncmp.php", +} diff --git a/src/builtins/string/strpos.rs b/src/builtins/string/strpos.rs index cdb73393ec..e0fcfb9122 100644 --- a/src/builtins/string/strpos.rs +++ b/src/builtins/string/strpos.rs @@ -5,9 +5,10 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - The declared signature carries the full golden param list (`haystack`, `needle`, -//! `offset`), but `max_args: 2` caps `check_arity` so a third argument is rejected, -//! matching the legacy CHECK arm which enforced exactly two arguments. +//! - The declared signature is PHP's own `strpos(string $haystack, string $needle, int $offset = 0)`. +//! `$offset` is accepted positionally and as a named argument; a negative value is resolved +//! against the haystack length, and an offset outside the haystack raises PHP's catchable +//! `ValueError` from the backend lowering. //! - `check` returns `PhpType::Union([Int, Bool])` (position, or `false` on no match). //! A check hook is required because the `builtin!` macro `returns:` field only accepts //! a simple type identifier and cannot express a union inline. Argument types are @@ -21,7 +22,6 @@ builtin! { name: "strpos", area: String, params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], - max_args: 2, returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -35,7 +35,7 @@ builtin! { /// /// A check hook is required because the `builtin!` macro cannot express a union return /// type inline. Argument types are inferred by the common registry dispatch path before -/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +/// this hook fires; arity is validated by the registry from the declared parameter list. fn check(_cx: &mut BuiltinCheckCtx) -> Result { Ok(PhpType::Union(vec![PhpType::Int, PhpType::False])) } diff --git a/src/builtins/string/strripos.rs b/src/builtins/string/strripos.rs new file mode 100644 index 0000000000..2aebd2487d --- /dev/null +++ b/src/builtins/string/strripos.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Home of the PHP `strripos` builtin: its single-source registry declaration and semantic target. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - The declared signature is PHP's own `strripos(string $haystack, string $needle, int $offset = 0)`, +//! the case-insensitive twin of `strrpos()`. A non-negative `$offset` starts the right-to-left +//! search at that byte; a negative one stops the search `-$offset` bytes before the haystack +//! end, and an out-of-haystack offset raises PHP's catchable `ValueError` from the backend +//! lowering — the shared `lower_string_position` path both spellings go through. +//! - Case folding is ASCII-only, matching php-src's locale-independent `zend_tolower_ascii`. +//! - `check` returns `PhpType::Union([Int, False])` (position, or `false` on no match). +//! A check hook is required because the `builtin!` macro `returns:` field only accepts +//! a simple type identifier and cannot express a union inline. Argument types are +//! inferred by the common registry dispatch path before the hook fires. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::types::PhpType; + +builtin! { + name: "strripos", + area: String, + params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], + returns: Mixed, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Strripos, + ), + summary: "Finds the numeric position of the last case-insensitive occurrence of a substring.", + php_manual: "https://www.php.net/manual/en/function.strripos.php", +} + +/// Returns `PhpType::Union([Int, Bool])` for a `strripos` call (position, or `false`). +/// +/// A check hook is required because the `builtin!` macro cannot express a union return +/// type inline. Argument types are inferred by the common registry dispatch path before +/// this hook fires; arity is validated by the registry from the declared parameter list. +fn check(_cx: &mut BuiltinCheckCtx) -> Result { + Ok(PhpType::Union(vec![PhpType::Int, PhpType::False])) +} diff --git a/src/builtins/string/strrpos.rs b/src/builtins/string/strrpos.rs index 51bf9de473..816b40a35b 100644 --- a/src/builtins/string/strrpos.rs +++ b/src/builtins/string/strrpos.rs @@ -5,9 +5,10 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - The declared signature carries the full golden param list (`haystack`, `needle`, -//! `offset`), but `max_args: 2` caps `check_arity` so a third argument is rejected, -//! matching the legacy CHECK arm which enforced exactly two arguments. +//! - The declared signature is PHP's own `strrpos(string $haystack, string $needle, int $offset = 0)`. +//! A non-negative `$offset` starts the right-to-left search at that byte; a negative one +//! stops the search `-$offset` bytes before the haystack end, and an out-of-haystack offset +//! raises PHP's catchable `ValueError` from the backend lowering. //! - `check` returns `PhpType::Union([Int, Bool])` (position, or `false` on no match). //! A check hook is required because the `builtin!` macro `returns:` field only accepts //! a simple type identifier and cannot express a union inline. Argument types are @@ -21,7 +22,6 @@ builtin! { name: "strrpos", area: String, params: [haystack: Str, needle: Str, offset: Int = DefaultSpec::Int(0)], - max_args: 2, returns: Mixed, check: check, semantics: crate::builtins::semantics::runtime_fn_semantics( @@ -35,7 +35,7 @@ builtin! { /// /// A check hook is required because the `builtin!` macro cannot express a union return /// type inline. Argument types are inferred by the common registry dispatch path before -/// this hook fires; arity (capped to 2 via `max_args`) is validated by the registry. +/// this hook fires; arity is validated by the registry from the declared parameter list. fn check(_cx: &mut BuiltinCheckCtx) -> Result { Ok(PhpType::Union(vec![PhpType::Int, PhpType::False])) } diff --git a/src/builtins/string/strtr.rs b/src/builtins/string/strtr.rs new file mode 100644 index 0000000000..9229a8e55b --- /dev/null +++ b/src/builtins/string/strtr.rs @@ -0,0 +1,95 @@ +//! Purpose: +//! Home of the PHP `strtr` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - php-src exposes one function with two shapes: `strtr($string, $from, $to)` translates +//! bytes pairwise (truncated to the shorter of `$from`/`$to`), and `strtr($string, $pairs)` +//! applies replacement pairs longest-match-first in a single left-to-right pass. +//! - `$from` is declared `Mixed` because it is `array|string` in php-src; the check hook +//! enforces php-src's own `TypeError` wording at compile time, where elephc can already see +//! the argument's type. +//! - The two-argument form needs string replacement VALUES: elephc reads them straight out of +//! the runtime hash instead of converting each one, so an array of non-string values is +//! rejected with an explicit diagnostic rather than silently mis-rendered. + +use crate::builtins::spec::{BuiltinCheckCtx, DefaultSpec}; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "strtr", + area: String, + params: [ + string: Str, + from: Mixed, + to: Str = DefaultSpec::Null + ], + returns: Str, + check: check, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::Strtr, + ), + summary: "Translates bytes pairwise, or applies longest-match-first replacement pairs.", + php_manual: "https://www.php.net/manual/en/function.strtr.php", +} + +/// Validates the `strtr` call shape and returns its `string` result type. +/// +/// Argument types are inferred by the common registry dispatch path before this hook fires, +/// and arity is pre-validated by the registry. The two-argument form requires an array +/// `$from` whose values are strings, and the three-argument form requires a string `$from`; +/// both mismatches carry php-src's own `TypeError` wording. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + let from = cx.checker.infer_type(from_argument(cx.args), cx.env)?; + if cx.args.len() >= 3 { + if matches!(from, PhpType::Array(_) | PhpType::AssocArray { .. }) { + return Err(CompileError::new( + cx.span, + "strtr(): Argument #2 ($from) must be of type string, array given", + )); + } + return Ok(PhpType::Str); + } + let values = match &from { + PhpType::Array(values) => values.as_ref().clone(), + PhpType::AssocArray { value, .. } => value.as_ref().clone(), + _ => { + return Err(CompileError::new( + cx.span, + "strtr(): Argument #2 ($from) must be of type array, string given", + )) + } + }; + if !matches!(values, PhpType::Str | PhpType::Never) { + return Err(CompileError::new( + cx.span, + "strtr() replacement values must be strings in AOT mode", + )); + } + Ok(PhpType::Str) +} + +/// Returns the `$from` argument expression from a call's source-order argument list. +/// +/// A `from:` named argument is matched by name first so `strtr($s, from: [...])` validates +/// like the positional spelling; otherwise the second positional argument is used. The +/// registry guarantees at least two arguments before this hook runs, so the caller always +/// gets an expression back. +fn from_argument(args: &[crate::parser::ast::Expr]) -> &crate::parser::ast::Expr { + for arg in args { + if let ExprKind::NamedArg { name, value } = &arg.kind { + if name == "from" { + return value; + } + } + } + args.iter() + .filter(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. })) + .nth(1) + .unwrap_or(&args[args.len() - 1]) +} diff --git a/src/builtins/string/substr_count.rs b/src/builtins/string/substr_count.rs new file mode 100644 index 0000000000..4a5da2ee5e --- /dev/null +++ b/src/builtins/string/substr_count.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Home of the PHP `substr_count` builtin: its declaration and semantic metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through +//! `crate::builtins::registry`. +//! +//! Key details: +//! - Matches php-src's signature `substr_count(string $haystack, string $needle, +//! int $offset = 0, ?int $length = null): int`, so `$length` is declared `Mixed` to +//! carry the nullable default while `$offset` stays a plain `int`. +//! - The typed runtime target carries `MAY_THROW`: an empty `$needle` and an `$offset` +//! or `$length` that escapes the subject each raise a catchable `ValueError`, so the +//! call must not be removable by dead-code elimination. + +use crate::builtins::spec::DefaultSpec; + +builtin! { + name: "substr_count", + area: String, + params: [ + haystack: Str, + needle: Str, + offset: Int = DefaultSpec::Int(0), + length: Mixed = DefaultSpec::Null + ], + returns: Int, + semantics: crate::builtins::semantics::runtime_fn_semantics( + crate::ir::RuntimeFnId::SubstrCount, + ), + summary: "Counts the number of non-overlapping substring occurrences.", + php_manual: "https://www.php.net/manual/en/function.substr-count.php", +} diff --git a/src/builtins/string/ucwords.rs b/src/builtins/string/ucwords.rs index c85e35adb9..ce4f83b14e 100644 --- a/src/builtins/string/ucwords.rs +++ b/src/builtins/string/ucwords.rs @@ -6,9 +6,9 @@ //! `crate::builtins::registry`. //! //! Key details: -//! - The declared signature carries the full golden param list (`string`, `separators`), -//! but `max_args: 1` caps `check_arity` so a second argument is rejected, matching the -//! legacy CHECK arm which enforced exactly one argument. +//! - The declared signature is PHP's own `ucwords(string $string, string $separators = " \t\r\n\f\v")`. +//! `$separators` is a byte SET, not a substring: every byte listed ends a word, and the +//! backend passes the default set explicitly when the argument is omitted. //! - No `check` hook is needed: the return type (`Str`) is fully determined by the //! declaration. The registry dispatch still infers each argument unconditionally, so //! undefined-variable diagnostics fire exactly as the legacy arm produced them. @@ -18,7 +18,6 @@ builtin! { name: "ucwords", area: String, params: [string: Str, separators: Str = crate::builtins::spec::DefaultSpec::Str(" \t\r\n\u{0c}\u{0b}")], - max_args: 1, returns: Str, semantics: crate::builtins::semantics::runtime_fn_semantics( crate::ir::RuntimeFnId::Ucwords, diff --git a/src/builtins/system/constant.rs b/src/builtins/system/constant.rs new file mode 100644 index 0000000000..423a99740e --- /dev/null +++ b/src/builtins/system/constant.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Home of the PHP `constant` builtin: its single-source registry declaration and semantic +//! metadata. +//! +//! Called from: +//! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. +//! +//! Key details: +//! - `check` mirrors `defined()`'s AOT contract: the constant NAME must be a compile-time string +//! literal. It also resolves the constant so the call's result type is the constant's own type +//! (`int`, `float`, `string`, `bool`, `null`) instead of `mixed`. +//! - An unknown name is a COMPILE error here, where reference PHP raises +//! `Error: Undefined constant "X"` at runtime. A binary with no constant table cannot look the +//! name up, and refusing at compile time is strictly more informative than a runtime fatal. +//! - Class constants and enum cases (`constant('Foo::BAR')`) are NOT supported: the name is +//! resolved through the global constant table only. +//! - Lowering happens one level up, in +//! `crate::ir_lower::expr::constants::lower_static_constant_call()`, which rewrites the call +//! into the same EIR a bare `FOO` reference produces. The registry lowering hook below is a +//! guard for paths that bypass that rewrite. + +use crate::builtins::semantics::{ + BuiltinCallablePolicy, BuiltinEffects, BuiltinLowering, BuiltinLoweringContext, + BuiltinLoweringError, BuiltinRequirements, BuiltinResultOwnership, BuiltinResultType, + BuiltinRuntimeFunctions, BuiltinSemantics, BuiltinTargetStrategy, BuiltinTargetSupport, + BuiltinValidation, LoweredBuiltinValue, NormalizedBuiltinCall, +}; +use crate::builtins::spec::BuiltinCheckCtx; +use crate::errors::CompileError; +use crate::parser::ast::ExprKind; +use crate::types::PhpType; + +builtin! { + name: "constant", + area: System, + params: [name: Str], + returns: Mixed, + check: check, + semantics: BuiltinSemantics { + validation: BuiltinValidation::SignatureOnly, + result_type: BuiltinResultType::Checked, + effects: BuiltinEffects::Static(crate::ir::Effects::READS_GLOBAL), + result_ownership: BuiltinResultOwnership::NonHeap, + requirements: BuiltinRequirements::Static(&[]), + target_strategy: BuiltinTargetStrategy::EirPrimitive, + target_support: BuiltinTargetSupport::All, + runtime_functions: BuiltinRuntimeFunctions::None, + argument_lowering: crate::builtins::semantics::BuiltinArgumentLowering::Standard, + callable: BuiltinCallablePolicy::StaticOnly( + "constant() needs a compile-time constant name", + ), + lowering: BuiltinLowering::Eir(lower), + }, + summary: "Returns the value of a constant given its name.", + php_manual: "https://www.php.net/manual/en/function.constant.php", +} + +/// Validates the literal name and returns the referenced constant's own PHP type. +/// +/// AOT compilation has no runtime constant table, so the name must be a `StringLiteral`. A +/// leading `\` is stripped the way PHP's own global-constant lookup does. Returns a +/// `CompileError` for a dynamic name, a class-constant name, or an unknown constant. +fn check(cx: &mut BuiltinCheckCtx) -> Result { + cx.checker.infer_type(&cx.args[0], cx.env)?; + let literal = match &cx.args[0].kind { + ExprKind::StringLiteral(name) => Some(name.clone()), + ExprKind::NamedArg { name, value } if name == "name" => match &value.kind { + ExprKind::StringLiteral(name) => Some(name.clone()), + _ => None, + }, + _ => None, + }; + let Some(name) = literal else { + return Err(CompileError::new( + cx.span, + "constant() first argument must be a string literal in AOT mode", + )); + }; + let name = name.trim_start_matches('\\').to_string(); + if name.contains("::") { + return Err(CompileError::new( + cx.span, + "constant() class constants are not supported; reference the constant directly", + )); + } + match cx.checker.constants.get(&name) { + Some(ty) => Ok(ty.clone()), + None => Err(CompileError::new( + cx.span, + &format!("Undefined constant: {}", name), + )), + } +} + +/// Rejects a `constant()` call that reached backend-neutral lowering. +/// +/// Direct calls are rewritten into a plain constant reference by +/// `crate::ir_lower::expr::constants::lower_static_constant_call()` before the registry +/// lowering runs, so reaching here means the name was not a literal — which `check` already +/// refuses for every path that type-checks. +fn lower( + _ctx: &mut dyn BuiltinLoweringContext, + call: &NormalizedBuiltinCall<'_>, +) -> Result { + Err(BuiltinLoweringError::new(format!( + "{}() needs a compile-time constant name", + call.name, + ))) +} diff --git a/src/builtins/system/mod.rs b/src/builtins/system/mod.rs index 4460ce767f..9e53da1f8a 100644 --- a/src/builtins/system/mod.rs +++ b/src/builtins/system/mod.rs @@ -42,6 +42,7 @@ pub mod class_get_attributes; pub mod date; pub mod date_default_timezone_get; pub mod date_default_timezone_set; +pub mod constant; pub mod define; pub mod defined; pub mod exec; diff --git a/src/builtins/types/intval.rs b/src/builtins/types/intval.rs index f638193a7a..fc3d161ea3 100644 --- a/src/builtins/types/intval.rs +++ b/src/builtins/types/intval.rs @@ -5,8 +5,14 @@ //! - Checker, EIR, optimizer, ownership, and callable consumers through `crate::builtins::registry`. //! //! Key details: -//! - Lowering reuses the general EIR integer cast instead of defining a builtin-specific opcode. -//! - Declared with exactly one parameter `value` (no `base` param) matching the legacy golden signature. +//! - The one-argument form lowers to the general EIR integer cast instead of a +//! builtin-specific opcode; only the two-argument form needs a runtime target. +//! - The declared signature is PHP's own `intval(mixed $value, int $base = 10)`. PHP applies +//! `$base` only when `$value` is a string and otherwise ignores it, which is why the +//! two-argument form lowers to `RuntimeFnId::IntvalBase` rather than a string-only helper: +//! the backend keeps the plain cast for non-string subjects. +//! - Reference PHP 8.4 raises nothing for an out-of-range `$base`; `strtol()` fails with +//! `EINVAL` and `intval("42", 1)` is simply `0`, so no `ValueError` is emitted here. use crate::builtins::semantics::{ BuiltinCallablePolicy, BuiltinEffects, BuiltinLowering, BuiltinLoweringContext, @@ -14,13 +20,14 @@ use crate::builtins::semantics::{ BuiltinRuntimeFunctions, BuiltinSemanticInput, BuiltinSemantics, BuiltinTargetStrategy, BuiltinTargetSupport, BuiltinValidation, LoweredBuiltinValue, NormalizedBuiltinCall, }; -use crate::ir::{Immediate, IrType, Op}; +use crate::builtins::spec::DefaultSpec; +use crate::ir::{Immediate, IrType, Op, RuntimeCallTarget, RuntimeFnId}; use crate::types::PhpType; builtin! { name: "intval", area: Types, - params: [value: Mixed], + params: [value: Mixed, base: Int = DefaultSpec::Int(10)], returns: Int, semantics: BuiltinSemantics { validation: BuiltinValidation::SignatureOnly, @@ -28,14 +35,14 @@ builtin! { effects: BuiltinEffects::Shared(effects), result_ownership: BuiltinResultOwnership::NonHeap, requirements: BuiltinRequirements::Static(&[]), - target_strategy: BuiltinTargetStrategy::EirPrimitive, + target_strategy: BuiltinTargetStrategy::EirGraph, target_support: BuiltinTargetSupport::All, - runtime_functions: BuiltinRuntimeFunctions::None, + runtime_functions: BuiltinRuntimeFunctions::One(RuntimeFnId::IntvalBase), argument_lowering: crate::builtins::semantics::BuiltinArgumentLowering::Standard, callable: BuiltinCallablePolicy::Dynamic(callable_accepts), lowering: BuiltinLowering::Eir(lower), }, - summary: "Returns the integer value of a variable.", + summary: "Returns the integer value of a variable, optionally using a given base.", php_manual: "function.intval", } @@ -61,11 +68,26 @@ fn callable_accepts(source: Option<&PhpType>) -> bool { }) } -/// Lowers `intval` through the reusable EIR integer-cast operation. +/// Lowers `intval` through the reusable EIR integer-cast operation, or through the +/// base-aware runtime target when PHP's `$base` argument is supplied. +/// +/// The single-argument spelling keeps the plain cast so the common case stays a primitive. +/// With a `$base` the whole decision moves to `RuntimeFnId::IntvalBase`, because PHP only +/// honors the base for string subjects and the subject's runtime type is not always known +/// here (a `Mixed` cell may or may not hold a string). fn lower( ctx: &mut dyn BuiltinLoweringContext, call: &NormalizedBuiltinCall<'_>, ) -> Result { + if call.operands.len() >= 2 { + return Ok(ctx.emit_runtime_call( + RuntimeCallTarget::Function(RuntimeFnId::IntvalBase), + vec![call.operand(0)?, call.operand(1)?], + call.result_type.clone(), + Op::Cast.default_effects(), + Some(call.span), + )); + } Ok(ctx.emit_value( Op::Cast, vec![call.operand(0)?], diff --git a/src/codegen/block_emit.rs b/src/codegen/block_emit.rs index 073955eba0..287ae78e23 100644 --- a/src/codegen/block_emit.rs +++ b/src/codegen/block_emit.rs @@ -38,8 +38,8 @@ use super::literal_defaults::{ emit_boxed_bool_literal_to_result, emit_boxed_float_literal_to_result, emit_boxed_int_literal_to_result, emit_boxed_null_literal_to_result, emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, - emit_string_literal_default_to_result, emit_tagged_null_literal_to_result, - literal_default_value, LiteralDefaultValue, + emit_string_literal_default_to_result, emit_tagged_int_literal_to_result, + emit_tagged_null_literal_to_result, literal_default_value, LiteralDefaultValue, }; use super::lower_inst; use super::lower_term; @@ -985,6 +985,9 @@ fn emit_static_property_default_value( LiteralDefaultValue::TaggedNull => { emit_tagged_null_literal_to_result(ctx); } + LiteralDefaultValue::TaggedInt(value) => { + emit_tagged_int_literal_to_result(ctx, *value); + } LiteralDefaultValue::BoxedNull => { emit_boxed_null_literal_to_result(ctx); } @@ -1039,8 +1042,8 @@ fn emit_blocks(ctx: &mut FunctionContext<'_>) -> Result<()> { /// Emits one EIR basic block. fn emit_block(ctx: &mut FunctionContext<'_>, block: &BasicBlock) -> Result<()> { ctx.emitter.comment(&format!("@block name={}", block.name)); - ctx.emitter - .label(&ctx.block_label(&block.name, block.id.as_raw())); + let block_label = ctx.block_label_for_id(block.id)?; + ctx.emitter.label(&block_label); for inst_id in &block.instructions { emit_instruction_source_marker(ctx, *inst_id)?; lower_inst::lower_instruction(ctx, *inst_id)?; diff --git a/src/codegen/context.rs b/src/codegen/context.rs index 3865f797b2..3dd0ba6ac3 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -9,6 +9,9 @@ //! Key details: //! - Phase 04 stores every SSA value in a stack slot and reloads result registers at use sites. //! - The context delegates target-specific movement to `crate::codegen::abi`. +//! - Local labels carry a module-unique trailing id from `SharedCodegenState::next_label_id()`. +//! The readable part is `crate::names::label_fragment()`, which is intentionally lossy, so the +//! id — not the fragment — is what keeps two similarly named functions from colliding. use std::collections::{HashMap, HashSet}; @@ -21,6 +24,7 @@ use crate::ir::{ ValueDef, ValueId, }; use crate::ir_passes::Allocation; +use crate::names::label_fragment; use crate::types::PhpType; use super::callable_reachability::CallableReachabilityAnalysis; @@ -63,7 +67,7 @@ pub(crate) struct FunctionContext<'a> { pub(super) gc_stats: bool, pub(super) heap_debug: bool, pub(super) epilogue_label: Option, - label_counter: usize, + block_labels: Vec, } impl<'a> FunctionContext<'a> { @@ -81,6 +85,20 @@ impl<'a> FunctionContext<'a> { epilogue_label: Option, ) -> Self { let callable_reachability = CallableReachabilityAnalysis::new(module, function); + let function_fragment = label_fragment(&function.name); + // Indexed by raw block id, matching `Function::block()`'s positional lookup. + let block_labels = function + .blocks + .iter() + .map(|block| { + format!( + "_eir_{}_{}_{}", + function_fragment, + label_fragment(&block.name), + shared.next_label_id() + ) + }) + .collect(); Self { module, function, @@ -105,20 +123,22 @@ impl<'a> FunctionContext<'a> { gc_stats, heap_debug, epilogue_label, - label_counter: 0, + block_labels, } } - /// Returns a unique local label with a readable prefix. + /// Returns a module-unique local label carrying a readable but lossy prefix. + /// + /// Uniqueness comes solely from the module-wide trailing id: `label_fragment()` collapses + /// every non-alphanumeric byte, so `a_b` and `aéb` share a readable prefix and only the id + /// keeps their labels apart. pub(super) fn next_label(&mut self, prefix: &str) -> String { - let label = format!( + format!( "_eir_{}_{}_{}", label_fragment(&self.function.name), label_fragment(prefix), - self.label_counter - ); - self.label_counter += 1; - label + self.shared.next_label_id() + ) } /// Emits an unconditional target-aware branch to one local assembly label. @@ -180,18 +200,16 @@ impl<'a> FunctionContext<'a> { Ok(()) } - /// Returns the assembly label for a non-entry EIR block. - pub(super) fn block_label(&self, block_name: &str, raw: u32) -> String { - format!("_eir_{}_{}_{}", label_fragment(&self.function.name), label_fragment(block_name), raw) - } - - /// Returns the assembly label for a block id. + /// Returns the assembly label reserved for one EIR block. + /// + /// Block labels are minted once per block in `new()` from the module-wide label counter + /// rather than derived from the block name, which is not unique across functions. Lookup is + /// positional on the raw block id, exactly like `crate::ir::Function::block()`. pub(super) fn block_label_for_id(&self, block: BlockId) -> Result { - let block = self - .function - .block(block) - .ok_or_else(|| CodegenIrError::missing_entry("block", block.as_raw()))?; - Ok(self.block_label(&block.name, block.id.as_raw())) + self.block_labels + .get(block.as_raw() as usize) + .cloned() + .ok_or_else(|| CodegenIrError::missing_entry("block", block.as_raw())) } /// Returns a module function by PHP name using PHP's case-insensitive lookup. @@ -1084,11 +1102,3 @@ fn emit_mixed_result_as_tagged_scalar(emitter: &mut Emitter) { } } } - -/// Converts arbitrary names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} diff --git a/src/codegen/enum_singletons.rs b/src/codegen/enum_singletons.rs index 2c58550ca5..02e672ec0d 100644 --- a/src/codegen/enum_singletons.rs +++ b/src/codegen/enum_singletons.rs @@ -75,7 +75,7 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::ir::Module; -use crate::names::enum_case_symbol; +use crate::names::{enum_case_symbol, join_php_symbol}; use crate::types::{ClassInfo, EnumCaseInfo, EnumCaseValue, EnumInfo}; use super::context::FunctionContext; @@ -107,22 +107,18 @@ const X86_64_SAVED: [&str; 9] = [ /// Returns the materializer symbol for one enum case of a PURE enum. /// -/// Format: `_enum_init__`. Reuses `enum_case_symbol`'s -/// mangling so the two symbols always agree on how a name was escaped. +/// Format: `_enum_init__`. Uses the same injective `join_php_symbol()` encoding as +/// `enum_case_symbol()` so the two symbols always agree on how a name was escaped. fn enum_case_init_symbol(enum_name: &str, case_name: &str) -> String { - format!("_enum_init{}", &enum_case_symbol(enum_name, case_name)[10..]) + join_php_symbol("_enum_init", &[enum_name, case_name]) } /// Returns the whole-enum materializer symbol used by BACKED enums. /// -/// Format: `_enum_init_all__` — derived from the -/// first case so it needs no separate mangling helper and cannot collide with a -/// per-case symbol. +/// Format: `_enum_init_all__` — derived from the first case so it needs no +/// separate mangling helper and cannot collide with a per-case symbol. fn enum_init_all_symbol(enum_name: &str, first_case: &str) -> String { - format!( - "_enum_init_all{}", - &enum_case_symbol(enum_name, first_case)[10..] - ) + join_php_symbol("_enum_init_all", &[enum_name, first_case]) } /// Returns the materializer to call before reading `enum_name::case_name`, or @@ -527,14 +523,14 @@ mod tests { #[test] fn materializer_symbols_track_the_slot_mangling() { let slot = enum_case_symbol("App\\Suit", "Hearts"); - assert_eq!(slot, "_enum_case_App_N_Suit_Hearts"); + assert_eq!(slot, "_enum_case___App_N_Suit___Hearts"); assert_eq!( enum_case_init_symbol("App\\Suit", "Hearts"), - "_enum_init_App_N_Suit_Hearts" + "_enum_init___App_N_Suit___Hearts" ); assert_eq!( enum_init_all_symbol("App\\Suit", "Hearts"), - "_enum_init_all_App_N_Suit_Hearts" + "_enum_init_all___App_N_Suit___Hearts" ); } diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs index 8ce2ca60bd..6c85526643 100644 --- a/src/codegen/eval_class_constant_helpers.rs +++ b/src/codegen/eval_class_constant_helpers.rs @@ -17,7 +17,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::ir::{Function, LocalKind, Module}; -use crate::names::php_symbol_key; +use crate::names::{join_php_symbol, php_symbol_key}; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Visibility}; use crate::types::{ClassInfo, InterfaceInfo, PhpType}; @@ -1494,17 +1494,20 @@ fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { } /// Returns a platform-safe body label for one class-constant slot. +/// +/// `mode` is a compiler-controlled literal and becomes part of the fixed prefix; the PHP names +/// go through `join_php_symbol()` so slots differing only in underscore placement stay distinct. fn slot_body_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> String { let suffix = match module.target.arch { Arch::AArch64 => "", Arch::X86_64 => "_x", }; format!( - "__elephc_eval_class_constant_{}_{}_{}_{}{}", - mode, - label_fragment(&slot.reflected_class), - label_fragment(&slot.declaring_class), - label_fragment(&slot.constant), + "{}{}", + join_php_symbol( + &format!("__elephc_eval_class_constant_{}", mode), + &[&slot.reflected_class, &slot.declaring_class, &slot.constant] + ), suffix ) } @@ -1514,13 +1517,6 @@ fn slot_miss_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> format!("{}_miss", slot_body_label(module, slot, mode)) } -/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits a C-global label using the target's symbol spelling. fn label_c_global(module: &Module, emitter: &mut Emitter, symbol: &str) { diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 97a112ddf6..b9c10baf01 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -23,7 +23,7 @@ use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; use crate::intrinsics::IntrinsicCall; use crate::ir::{Function, LocalKind, Module}; -use crate::names::{method_symbol, static_method_symbol}; +use crate::names::{join_php_symbol, method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -808,10 +808,7 @@ fn emit_aarch64_static_method_dispatch( fail_label: &str, ) { for (class_name, class_slots) in grouped_static_slots(slots) { - let next_label = format!( - "__elephc_eval_static_method_next_{}", - label_fragment(class_name) - ); + let next_label = join_php_symbol("__elephc_eval_static_method_next", &[class_name]); emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { emit_aarch64_static_method_name_compare(module, emitter, data, slot, fail_label); @@ -830,8 +827,8 @@ fn emit_x86_64_static_method_dispatch( ) { for (class_name, class_slots) in grouped_static_slots(slots) { let next_label = format!( - "__elephc_eval_static_method_next_{}_x", - label_fragment(class_name) + "{}_x", + join_php_symbol("__elephc_eval_static_method_next", &[class_name]) ); emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { @@ -2309,16 +2306,20 @@ fn grouped_static_slots( } /// Returns a platform-safe body label for a method slot. +/// +/// The class/impl-class/method triplet is joined through `join_php_symbol()` so two slots whose +/// names differ only in where an underscore falls cannot land on the same label. fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { let suffix = match module.target.arch { Arch::AArch64 => "", Arch::X86_64 => "_x", }; format!( - "__elephc_eval_method_{}_{}_{}{}", - label_fragment(&slot.class_name), - label_fragment(&slot.impl_class), - label_fragment(&slot.method), + "{}{}", + join_php_symbol( + "__elephc_eval_method", + &[&slot.class_name, &slot.impl_class, &slot.method] + ), suffix ) } @@ -2329,16 +2330,19 @@ fn method_access_miss_label(module: &Module, slot: &EvalMethodSlot) -> String { } /// Returns a platform-safe body label for a static method slot. +/// +/// Uses the same injective join as `method_body_label()` under a distinct symbol prefix. fn static_method_body_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { let suffix = match module.target.arch { Arch::AArch64 => "", Arch::X86_64 => "_x", }; format!( - "__elephc_eval_static_method_{}_{}_{}{}", - label_fragment(&slot.class_name), - label_fragment(&slot.impl_class), - label_fragment(&slot.method), + "{}{}", + join_php_symbol( + "__elephc_eval_static_method_body", + &[&slot.class_name, &slot.impl_class, &slot.method] + ), suffix ) } @@ -2404,13 +2408,6 @@ fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { .unwrap_or(u64::MAX) } -/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 2bbcecfdfe..4d576f7429 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -20,6 +20,7 @@ use crate::codegen::runtime_value_tag; use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::ir::{Function, LocalKind, Module}; +use crate::names::join_php_symbol; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -898,7 +899,7 @@ fn emit_aarch64_uninitialized_property_get_guard( } let initialized_label = format!( "{}_initialized", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer for marker inspection emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker @@ -921,7 +922,7 @@ fn emit_x86_64_uninitialized_property_get_guard( } let initialized_label = format!( "{}_initialized_x", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for marker inspection emitter.instruction(&format!("mov rax, QWORD PTR [r10 + {}]", slot.offset + 8)); // load the typed-property initialization marker @@ -968,11 +969,11 @@ fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot PhpType::Mixed | PhpType::Union(_) => { let null_label = format!( "{}_mixed_null", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); let done_label = format!( "{}_mixed_done", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the stored Mixed property cell emitter.instruction(&format!("cbz x0, {}", null_label)); // null property storage reads as PHP null @@ -1019,8 +1020,8 @@ fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); } PhpType::Mixed | PhpType::Union(_) => { - let null_label = format!("{}_mixed_null_x", label_fragment(&slot_body_label_raw(slot, "get"))); - let done_label = format!("{}_mixed_done_x", label_fragment(&slot_body_label_raw(slot, "get"))); + let null_label = format!("{}_mixed_null_x", slot_body_label_raw(slot, "get")); + let done_label = format!("{}_mixed_done_x", slot_body_label_raw(slot, "get")); emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset)); // load the stored Mixed property cell emitter.instruction("test rax, rax"); // check whether the property storage is initialized emitter.instruction(&format!("jz {}", null_label)); // null property storage reads as PHP null @@ -1175,11 +1176,11 @@ fn emit_aarch64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalP fn emit_aarch64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { let null_label = format!( "{}_tagged_scalar_null", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); let done_label = format!( "{}_tagged_scalar_done", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for nullable-int inspection emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words @@ -1305,11 +1306,11 @@ fn emit_x86_64_store_object_property_slot( fn emit_x86_64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { let null_label = format!( "{}_tagged_scalar_null_x", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); let done_label = format!( "{}_tagged_scalar_done_x", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for nullable-int inspection emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words @@ -1359,13 +1360,13 @@ fn slot_scope_ok_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> } /// Returns the architecture-independent body label stem for a property slot. +/// +/// `mode` is a compiler-controlled literal and becomes part of the fixed prefix; the PHP names +/// go through `join_php_symbol()` so slots differing only in underscore placement stay distinct. fn slot_body_label_raw(slot: &EvalPropertySlot, mode: &str) -> String { - format!( - "__elephc_eval_property_{}_{}_{}_{}", - mode, - label_fragment(&slot.class_name), - label_fragment(&slot.declaring_class), - label_fragment(&slot.property) + join_php_symbol( + &format!("__elephc_eval_property_{}", mode), + &[&slot.class_name, &slot.declaring_class, &slot.property], ) } @@ -1425,13 +1426,6 @@ fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { .unwrap_or(u64::MAX) } -/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index 8073517d82..881b5cd6fa 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -21,7 +21,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; use crate::ir::{Function, LocalKind, Module}; -use crate::names::static_property_symbol; +use crate::names::{join_php_symbol, static_property_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -394,10 +394,9 @@ fn emit_aarch64_static_property_dispatch( mode: &str, ) { for (class_name, class_slots) in grouped_slots(slots) { - let next_label = format!( - "__elephc_eval_static_property_{}_next_{}", - mode, - label_fragment(class_name) + let next_label = join_php_symbol( + &format!("__elephc_eval_static_property_{}_next", mode), + &[class_name], ); emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { @@ -417,9 +416,11 @@ fn emit_x86_64_static_property_dispatch( ) { for (class_name, class_slots) in grouped_slots(slots) { let next_label = format!( - "__elephc_eval_static_property_{}_next_{}_x", - mode, - label_fragment(class_name) + "{}_x", + join_php_symbol( + &format!("__elephc_eval_static_property_{}_next", mode), + &[class_name], + ) ); emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { @@ -715,7 +716,7 @@ fn emit_aarch64_uninitialized_guard( } let initialized_label = format!( "{}_initialized", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); abi::emit_load_symbol_to_reg(emitter, "x10", &slot.symbol, 8); abi::emit_load_int_immediate(emitter, "x11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); @@ -737,7 +738,7 @@ fn emit_x86_64_uninitialized_guard( } let initialized_label = format!( "{}_initialized_x", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); abi::emit_load_symbol_to_reg(emitter, "r10", &slot.symbol, 8); abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); @@ -772,11 +773,11 @@ fn emit_aarch64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStati PhpType::Mixed | PhpType::Union(_) => { let null_label = format!( "{}_mixed_null", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); let done_label = format!( "{}_mixed_done", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); emitter.instruction(&format!("cbz x0, {}", null_label)); // null static storage reads as PHP null @@ -818,11 +819,11 @@ fn emit_x86_64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStatic PhpType::Mixed | PhpType::Union(_) => { let null_label = format!( "{}_mixed_null_x", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); let done_label = format!( "{}_mixed_done_x", - label_fragment(&slot_body_label_raw(slot, "get")) + slot_body_label_raw(slot, "get") ); abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); emitter.instruction("test rax, rax"); // check whether static storage holds a Mixed cell @@ -1055,11 +1056,11 @@ fn emit_aarch64_store_tagged_scalar_static_property( ) { let null_label = format!( "{}_tagged_scalar_null", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); let done_label = format!( "{}_tagged_scalar_done", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for nullable-int inspection emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words @@ -1083,11 +1084,11 @@ fn emit_x86_64_store_tagged_scalar_static_property( ) { let null_label = format!( "{}_tagged_scalar_null_x", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); let done_label = format!( "{}_tagged_scalar_done_x", - label_fragment(&slot_body_label_raw(slot, "set")) + slot_body_label_raw(slot, "set") ); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for nullable-int inspection emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words @@ -1145,13 +1146,13 @@ fn slot_access_miss_label( } /// Returns the architecture-independent body label stem for a static property slot. +/// +/// `mode` is a compiler-controlled literal and becomes part of the fixed prefix; the PHP names +/// go through `join_php_symbol()` so slots differing only in underscore placement stay distinct. fn slot_body_label_raw(slot: &EvalStaticPropertySlot, mode: &str) -> String { - format!( - "__elephc_eval_static_property_{}_{}_{}_{}", - mode, - label_fragment(&slot.class_name), - label_fragment(&slot.declaring_class), - label_fragment(&slot.property) + join_php_symbol( + &format!("__elephc_eval_static_property_{}", mode), + &[&slot.class_name, &slot.declaring_class, &slot.property], ) } @@ -1211,13 +1212,6 @@ fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { .unwrap_or(u64::MAX) } -/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits a C-visible global label with target-specific symbol mangling. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index 8ed6c48c1b..6658b15841 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -27,6 +27,7 @@ use crate::types::PhpType; use super::context::FunctionContext; use super::local_analysis::LocalSlotAnalysis; +use super::stack_guard; use super::value_placement::{self, ValuePlacement}; const FRAME_FOOTER_BYTES: usize = 16; @@ -234,6 +235,10 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { emit_callee_saved_saves(ctx); ctx.emitter.comment("save argc/argv to globals"); abi::emit_store_process_args_to_globals(ctx.emitter); + // Measure the stack only after argc/argv are safe in globals: the initializer is an + // ordinary call and clobbers the C-ABI argument registers they arrive in. `main` itself + // is never guarded — it is the root of every call chain and runs before the floor exists. + stack_guard::emit_stack_limit_init_call(ctx.emitter); if ctx.heap_debug { ctx.emitter.comment("enable heap debug flag"); abi::emit_enable_heap_debug_flag(ctx.emitter); @@ -260,6 +265,12 @@ pub(super) fn emit_function_prologue_with_label( ctx.emitter.blank(); ctx.emitter.label_global(entry_label); abi::emit_frame_prologue(ctx.emitter, ctx.frame_size); + // The depth check runs before anything is written to the new frame and before the + // incoming arguments are spilled, so it only needs x9 (AArch64) / no register at all + // (x86_64) and cannot disturb the ABI. Placing it after the frame has been reserved + // means the compare already accounts for this function's own frame size. + let stack_ok_label = ctx.next_label("stack_ok"); + stack_guard::emit_stack_limit_check(ctx.emitter, &stack_ok_label); capture_concat_base(ctx); emit_callee_saved_saves(ctx); @@ -504,6 +515,10 @@ pub(super) fn emit_web_entry_stub(ctx: &mut FunctionContext<'_>) { ctx.emitter .comment("save argc/argv to globals for the bridge and handler"); abi::emit_store_process_args_to_globals(ctx.emitter); + // `--web` forks its workers from this process and each worker serves requests on its own + // main stack, so the floor measured here stays valid in every child. Measuring before the + // bridge call also keeps the clobbered argument registers away from `elephc_web_run`. + stack_guard::emit_stack_limit_init_call(ctx.emitter); // Enable the small-bin double-free guard for every --web worker process: a detected // double free `_exit(1)`s the worker (the prefork master respawns it), containing // corruption to one request. Cheap — a short bin-chain scan on free, with no diff --git a/src/codegen/literal_defaults.rs b/src/codegen/literal_defaults.rs index cbcfe18de4..aeb2afb377 100644 --- a/src/codegen/literal_defaults.rs +++ b/src/codegen/literal_defaults.rs @@ -12,6 +12,10 @@ //! elements, empty object-typed indexed arrays, and associative-array literals //! (empty, positional, or with constant integer/string keys and scalar/string/null //! values) land here. +//! - The declared PHP type selects the storage shape, and slot-shape arms must precede the +//! generic `Mixed`/`Union(_)` boxing arms. A null-capable int slot (`?int` under +//! `NullRepr::Tagged`) is an inline two-word `{payload, tag}` TaggedScalar, so it takes +//! `TaggedInt`/`TaggedNull` and never a boxed Mixed pointer. use crate::codegen::platform::Arch; use crate::codegen::{ @@ -34,6 +38,7 @@ pub(crate) enum LiteralDefaultValue { Null, NullSentinel, TaggedNull, + TaggedInt(i64), BoxedNull, BoxedInt(i64), BoxedBool(bool), @@ -104,6 +109,29 @@ pub(crate) fn literal_default_value( _ => Err(unsupported_literal_default(context, php_type, op_name)), }, (PhpType::Str, ExprKind::StringLiteral(value)) => Ok(LiteralDefaultValue::Str(value.clone())), + // A null-capable int slot (`?int` / `int|null` under `NullRepr::Tagged`) is stored inline + // as the two-word `{payload, tag}` TaggedScalar, never as a pointer to a boxed Mixed cell. + // These arms must stay ahead of the `Mixed | Union(_)` boxing arms below: an `int|null` + // property type also matches `Union(_)`, and boxing it would write a Mixed pointer into the + // payload word while the tag word still reads "int", so the reader hands back the pointer + // as an integer. + (php_type, ExprKind::IntLiteral(value)) + if php_type.codegen_repr() == PhpType::TaggedScalar => + { + Ok(LiteralDefaultValue::TaggedInt(*value)) + } + (php_type, ExprKind::Negate(inner)) if php_type.codegen_repr() == PhpType::TaggedScalar => { + match &inner.kind { + ExprKind::IntLiteral(value) => value + .checked_neg() + .map(LiteralDefaultValue::TaggedInt) + .ok_or_else(|| unsupported_literal_default(context, php_type, op_name)), + _ => Err(unsupported_literal_default(context, php_type, op_name)), + } + } + (php_type, ExprKind::Null) if php_type.codegen_repr() == PhpType::TaggedScalar => { + Ok(LiteralDefaultValue::TaggedNull) + } (PhpType::Mixed | PhpType::Union(_), ExprKind::StringLiteral(value)) => { Ok(LiteralDefaultValue::BoxedStr(value.clone())) } @@ -126,9 +154,6 @@ pub(crate) fn literal_default_value( ExprKind::FloatLiteral(value) => Ok(LiteralDefaultValue::BoxedFloat(-value)), _ => Err(unsupported_literal_default(context, php_type, op_name)), }, - (php_type, ExprKind::Null) if php_type.codegen_repr() == PhpType::TaggedScalar => { - Ok(LiteralDefaultValue::TaggedNull) - } (PhpType::Mixed | PhpType::Union(_), ExprKind::Null) => Ok(LiteralDefaultValue::BoxedNull), (PhpType::Void | PhpType::Never, ExprKind::Null) => Ok(LiteralDefaultValue::NullSentinel), (PhpType::Void | PhpType::Never, _) => Ok(LiteralDefaultValue::NullSentinel), @@ -280,6 +305,14 @@ pub(crate) fn emit_tagged_null_literal_to_result(ctx: &mut FunctionContext<'_>) crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); } +/// Emits a non-null integer literal default as an inline tagged scalar: the immediate lands in +/// the integer result register (the payload word) and the int runtime tag in the adjacent tag +/// register, which is exactly the `{payload, tag}` pair a `?int` slot is read back through. +pub(crate) fn emit_tagged_int_literal_to_result(ctx: &mut FunctionContext<'_>, value: i64) { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), value); + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(ctx.emitter); +} + /// Emits an indexed-array literal default into the canonical result register. pub(super) fn emit_array_literal_default_to_result( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index f52f4b9511..1b48dd370e 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -24,7 +24,8 @@ use crate::ir::{ IrType, LocalKind, LocalSlotId, Module, Op, Ownership, Terminator, ValueDef, ValueId, }; use crate::names::{ - function_symbol, ir_global_symbol, method_symbol, php_symbol_key, static_method_symbol, + function_symbol, ir_global_symbol, method_symbol, php_symbol_key, + static_method_symbol, }; use crate::types::{callable_wrapper_sig, first_class_callable_builtin_sig, FunctionSig, PhpType}; @@ -51,6 +52,7 @@ mod ownership; mod pointers; mod predicates; mod property_values; +mod receiver_place; mod runtime_calls; mod scoped_constants; mod static_locals; @@ -174,6 +176,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ICheckedAdd => arithmetic::lower_int_checked_binop(ctx, &inst, "__rt_int_add_checked"), Op::ICheckedSub => arithmetic::lower_int_checked_binop(ctx, &inst, "__rt_int_sub_checked"), Op::ICheckedMul => arithmetic::lower_int_checked_binop(ctx, &inst, "__rt_int_mul_checked"), + Op::ICheckedPow => arithmetic::lower_int_checked_binop(ctx, &inst, "__rt_int_pow_checked"), Op::IDiv => arithmetic::lower_int_div_to_float(ctx, &inst), Op::ISMod => arithmetic::lower_int_mod(ctx, &inst), Op::INeg => arithmetic::lower_int_unary(ctx, &inst, "neg", "neg"), @@ -181,13 +184,14 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::IBitOr => arithmetic::lower_int_binop(ctx, &inst, "orr", "or"), Op::IBitXor => arithmetic::lower_int_binop(ctx, &inst, "eor", "xor"), Op::IBitNot => arithmetic::lower_int_unary(ctx, &inst, "mvn", "not"), - Op::IShl => arithmetic::lower_int_shift(ctx, &inst, "lsl", "shl"), - Op::IShrA => arithmetic::lower_int_shift(ctx, &inst, "asr", "sar"), + Op::IShl => arithmetic::lower_int_shift(ctx, &inst, true), + Op::IShrA => arithmetic::lower_int_shift(ctx, &inst, false), Op::MixedNumericBinop => arithmetic::lower_mixed_numeric_binop(ctx, &inst), + Op::StrIncDec => strings::lower_str_inc_dec(ctx, &inst), Op::FAdd => floats::lower_float_binop(ctx, &inst, "fadd", "addsd"), Op::FSub => floats::lower_float_binop(ctx, &inst, "fsub", "subsd"), Op::FMul => floats::lower_float_binop(ctx, &inst, "fmul", "mulsd"), - Op::FDiv => floats::lower_float_binop(ctx, &inst, "fdiv", "divsd"), + Op::FDiv => arithmetic::lower_float_div(ctx, &inst), Op::FPow => floats::lower_float_pow(ctx, &inst), Op::FNeg => floats::lower_float_neg(ctx, &inst), Op::ICmp => lower_int_compare(ctx, &inst), @@ -291,6 +295,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::NullsafePropGet => objects::lower_nullsafe_prop_get(ctx, &inst), Op::DynamicPropGet => objects::lower_dynamic_prop_get(ctx, &inst), Op::PropSet => objects::lower_prop_set(ctx, &inst), + Op::PropUnset => objects::lower_prop_unset(ctx, &inst), Op::DynamicPropSet => objects::lower_dynamic_prop_set(ctx, &inst), Op::InstanceOf => objects::lower_instanceof(ctx, &inst), Op::InstanceOfDynamic => objects::lower_instanceof_dynamic(ctx, &inst), diff --git a/src/codegen/lower_inst/arithmetic.rs b/src/codegen/lower_inst/arithmetic.rs index 89f4379c1d..092ac2e787 100644 --- a/src/codegen/lower_inst/arithmetic.rs +++ b/src/codegen/lower_inst/arithmetic.rs @@ -15,7 +15,9 @@ use crate::ir::{Immediate, Instruction, MixedNumericOp, ValueId}; use crate::types::PhpType; use super::super::context::FunctionContext; -use super::{expect_operand, require_integer_like, store_if_result}; +use super::{ + expect_operand, require_float, require_integer_like, secondary_float_reg, store_if_result, +}; use crate::codegen::{CodegenIrError, Result}; /// Lowers a two-operand integer arithmetic or bitwise instruction. @@ -77,7 +79,20 @@ pub(super) fn lower_int_checked_binop( store_if_result(ctx, inst) } -/// Lowers a signed integer modulo operation with the established zero-divisor guard. +/// The php-src wording for a zero divisor in `%` / `%=`. +const MODULO_BY_ZERO_MESSAGE: &str = "Modulo by zero"; +/// The php-src wording for a zero divisor in `/` / `/=`. +const DIVISION_BY_ZERO_MESSAGE: &str = "Division by zero"; +/// The php-src wording for `<<` / `>>` with a negative shift count. +const NEGATIVE_SHIFT_MESSAGE: &str = "Bit shift by negative number"; + +/// Lowers a signed integer modulo operation with PHP's zero-divisor and overflow guards. +/// +/// Reference PHP 8.4 raises a catchable `DivisionByZeroError("Modulo by zero")` for `$x % 0` +/// instead of producing a value, and evaluates `PHP_INT_MIN % -1` to `0`. The x86_64 `idiv` +/// instruction traps with `#DE` (SIGFPE) on that second case, so `-1` divisors are answered +/// without ever reaching the divide unit. AArch64's `sdiv`/`msub` pair already wraps to `0` +/// there, matching PHP. pub(super) fn lower_int_mod(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; @@ -90,30 +105,37 @@ pub(super) fn lower_int_mod(ctx: &mut FunctionContext<'_>, inst: &Instruction) - match ctx.emitter.target.arch { Arch::AArch64 => { let quotient_reg = abi::tertiary_scratch_reg(ctx.emitter); - ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // branch to zero-divisor guard when modulo divisor is zero + ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // branch to the zero-divisor throw when the modulo divisor is zero ctx.emitter.instruction(&format!("sdiv {}, {}, {}", quotient_reg, result_reg, rhs_reg)); // compute signed quotient for the modulo operation ctx.emitter.instruction(&format!("msub {}, {}, {}, {}", result_reg, quotient_reg, rhs_reg, result_reg)); // compute left - quotient * right as the remainder - ctx.emitter.instruction(&format!("b {}", done_label)); // skip the modulo zero fallback after a normal remainder - ctx.emitter.label(&zero_label); - ctx.emitter.instruction(&format!("mov {}, #0", result_reg)); // return zero for modulo by zero - ctx.emitter.label(&done_label); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the zero-divisor throw after a normal remainder } Arch::X86_64 => { + let neg_one_label = ctx.next_label("mod_neg_one"); ctx.emitter.instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the modulo divisor is zero - ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to zero-divisor guard when modulo divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to the zero-divisor throw when the modulo divisor is zero + ctx.emitter.instruction(&format!("cmp {}, -1", rhs_reg)); // test whether the modulo divisor is -1 + ctx.emitter.instruction(&format!("je {}", neg_one_label)); // PHP_INT_MIN % -1 would raise #DE, and every x % -1 is zero anyway ctx.emitter.instruction("cqo"); // sign-extend the dividend before signed division ctx.emitter.instruction(&format!("idiv {}", rhs_reg)); // divide signed integers with quotient in rax and remainder in rdx ctx.emitter.instruction(&format!("mov {}, rdx", result_reg)); // move the signed remainder into the integer result register - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the modulo zero fallback after a normal remainder - ctx.emitter.label(&zero_label); - ctx.emitter.instruction(&format!("mov {}, 0", result_reg)); // return zero for modulo by zero - ctx.emitter.label(&done_label); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the guard blocks after a normal remainder + ctx.emitter.label(&neg_one_label); + ctx.emitter.instruction(&format!("mov {}, 0", result_reg)); // every integer modulo -1 is zero, exactly like PHP + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the zero-divisor throw after the -1 shortcut } } + ctx.emitter.label(&zero_label); + super::exceptions::emit_division_by_zero_error(ctx, MODULO_BY_ZERO_MESSAGE); + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } /// Lowers PHP `/` for integer operands by promoting both sides to floating point. +/// +/// Reference PHP 8.4 raises a catchable `DivisionByZeroError("Division by zero")` for a zero +/// divisor, so the hardware quotient (`INF` / `NaN`) is never observable. The guard runs before +/// the promotion for both supported targets. pub(super) fn lower_int_div_to_float( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -124,18 +146,67 @@ pub(super) fn lower_int_div_to_float( let rhs_reg = abi::tertiary_scratch_reg(ctx.emitter); load_integer_operand(ctx, lhs, lhs_reg, inst)?; load_integer_operand(ctx, rhs, rhs_reg, inst)?; + let zero_label = ctx.next_label("div_zero"); + let done_label = ctx.next_label("div_done"); match ctx.emitter.target.arch { Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); // branch to the zero-divisor throw when the divisor is zero ctx.emitter.instruction(&format!("scvtf d0, {}", lhs_reg)); // promote the integer dividend into the float result register ctx.emitter.instruction(&format!("scvtf d1, {}", rhs_reg)); // promote the integer divisor into a float scratch register ctx.emitter.instruction("fdiv d0, d0, d1"); // divide promoted operands as PHP floating-point division + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the zero-divisor throw after a normal quotient } Arch::X86_64 => { + ctx.emitter.instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to the zero-divisor throw when the divisor is zero ctx.emitter.instruction(&format!("cvtsi2sd xmm0, {}", lhs_reg)); // promote the integer dividend into the float result register ctx.emitter.instruction(&format!("cvtsi2sd xmm1, {}", rhs_reg)); // promote the integer divisor into a float scratch register ctx.emitter.instruction("divsd xmm0, xmm1"); // divide promoted operands as PHP floating-point division + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the zero-divisor throw after a normal quotient } } + ctx.emitter.label(&zero_label); + super::exceptions::emit_division_by_zero_error(ctx, DIVISION_BY_ZERO_MESSAGE); + ctx.emitter.label(&done_label); + store_if_result(ctx, inst) +} + +/// Lowers PHP `/` for floating-point operands with the PHP zero-divisor guard. +/// +/// Reference PHP 8.4 raises `DivisionByZeroError` for `1.0 / 0`, `1 / 0.0`, and `0.0 / 0.0` +/// alike — the IEEE result (`INF` / `NaN`) is never observable through the `/` operator. Only +/// `fdiv()` returns it. Both `+0.0` and `-0.0` divisors throw and a `NaN` divisor does not, so +/// AArch64 uses `fcmp`'s zero form (unordered leaves `eq` clear) and x86_64 shifts the sign bit +/// out of the raw bit pattern, which is zero for `±0.0` only. +pub(super) fn lower_float_div(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let lhs = expect_operand(inst, 0)?; + let rhs = expect_operand(inst, 1)?; + let lhs_reg = secondary_float_reg(ctx.emitter.target.arch); + let rhs_reg = abi::float_result_reg(ctx.emitter); + require_float(ctx.load_value_to_reg(lhs, lhs_reg)?, inst)?; + require_float(ctx.load_value_to_reg(rhs, rhs_reg)?, inst)?; + let zero_label = ctx.next_label("fdiv_zero"); + let done_label = ctx.next_label("fdiv_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("fcmp d0, #0.0"); // compare the divisor with zero; NaN stays unordered and divides normally + ctx.emitter.instruction(&format!("b.eq {}", zero_label)); // branch to the zero-divisor throw for both +0.0 and -0.0 + ctx.emitter.instruction("fdiv d0, d1, d0"); // divide the dividend by the divisor into the float result register + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the zero-divisor throw after a normal quotient + } + Arch::X86_64 => { + let bits_reg = abi::secondary_scratch_reg(ctx.emitter); + ctx.emitter.instruction(&format!("movq {}, xmm0", bits_reg)); // raw IEEE-754 bits of the divisor + ctx.emitter.instruction(&format!("add {}, {}", bits_reg, bits_reg)); // shift out the sign bit so -0.0 tests equal to +0.0 (NaN stays non-zero) + ctx.emitter.instruction(&format!("jz {}", zero_label)); // branch to the zero-divisor throw for both +0.0 and -0.0 + ctx.emitter.instruction("divsd xmm1, xmm0"); // divide the dividend by the divisor in the float scratch register + ctx.emitter.instruction("movsd xmm0, xmm1"); // move the quotient into the float result register + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the zero-divisor throw after a normal quotient + } + } + ctx.emitter.label(&zero_label); + super::exceptions::emit_division_by_zero_error(ctx, DIVISION_BY_ZERO_MESSAGE); + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -160,12 +231,21 @@ pub(super) fn lower_int_unary( store_if_result(ctx, inst) } -/// Lowers a variable-count signed integer shift operation. +/// Lowers a variable-count signed integer shift operation with PHP's shift-count rules. +/// +/// Raw AArch64 (`lsl`/`asr`) and x86_64 (`shl`/`sar`) register shifts mask the count to its low +/// six bits, which is *not* what PHP does. Reference PHP 8.4: +/// - a negative shift count raises a catchable `ArithmeticError("Bit shift by negative number")`; +/// - `<<` by 64 or more yields `0`; +/// - `>>` by 64 or more yields `0` for a non-negative value and `-1` for a negative one, i.e. the +/// arithmetic shift saturates at a full sign fill. +/// +/// `left` selects `<<` (logical left shift, saturating to zero) from `>>` (arithmetic right +/// shift, saturating to the sign fill). Both branches are emitted identically on both targets. pub(super) fn lower_int_shift( ctx: &mut FunctionContext<'_>, inst: &Instruction, - aarch64_mnemonic: &str, - x86_64_mnemonic: &str, + left: bool, ) -> Result<()> { let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; @@ -173,15 +253,46 @@ pub(super) fn lower_int_shift( let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); load_integer_operand(ctx, lhs, result_reg, inst)?; load_integer_operand(ctx, rhs, rhs_reg, inst)?; + let negative_label = ctx.next_label("shift_negative"); + let saturate_label = ctx.next_label("shift_saturate"); + let done_label = ctx.next_label("shift_done"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("{} {}, {}, {}", aarch64_mnemonic, result_reg, result_reg, rhs_reg)); // shift the integer operand by the EIR count operand + let mnemonic = if left { "lsl" } else { "asr" }; + ctx.emitter.instruction(&format!("tbnz {}, #63, {}", rhs_reg, negative_label)); // a negative shift count is an ArithmeticError in PHP + ctx.emitter.instruction(&format!("cmp {}, #64", rhs_reg)); // is the shift count outside the 64-bit window? + ctx.emitter.instruction(&format!("b.hs {}", saturate_label)); // PHP saturates instead of masking the count to 6 bits + ctx.emitter.instruction(&format!("{} {}, {}, {}", mnemonic, result_reg, result_reg, rhs_reg)); // shift the integer operand by the EIR count operand + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the saturation and throw blocks after a normal shift + ctx.emitter.label(&saturate_label); + if left { + ctx.emitter.instruction(&format!("mov {}, #0", result_reg)); // every bit is shifted out, so PHP yields 0 + } else { + ctx.emitter.instruction(&format!("asr {}, {}, #63", result_reg, result_reg)); // PHP fills with the sign bit: 0 for non-negative, -1 for negative + } + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the throw block after saturating } Arch::X86_64 => { + let mnemonic = if left { "shl" } else { "sar" }; + ctx.emitter.instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // inspect the sign of the shift count + ctx.emitter.instruction(&format!("js {}", negative_label)); // a negative shift count is an ArithmeticError in PHP + ctx.emitter.instruction(&format!("cmp {}, 64", rhs_reg)); // is the shift count outside the 64-bit window? + ctx.emitter.instruction(&format!("jge {}", saturate_label)); // PHP saturates instead of masking the count to 6 bits ctx.emitter.instruction(&format!("mov rcx, {}", rhs_reg)); // move the variable shift count into x86_64's required cl register - ctx.emitter.instruction(&format!("{} {}, cl", x86_64_mnemonic, result_reg)); // shift the integer operand by the low count byte + ctx.emitter.instruction(&format!("{} {}, cl", mnemonic, result_reg)); // shift the integer operand by the low count byte + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the saturation and throw blocks after a normal shift + ctx.emitter.label(&saturate_label); + if left { + ctx.emitter.instruction(&format!("mov {}, 0", result_reg)); // every bit is shifted out, so PHP yields 0 + } else { + ctx.emitter.instruction(&format!("sar {}, 63", result_reg)); // PHP fills with the sign bit: 0 for non-negative, -1 for negative + } + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the throw block after saturating } } + ctx.emitter.label(&negative_label); + super::exceptions::emit_arithmetic_error(ctx, NEGATIVE_SHIFT_MESSAGE); + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -302,5 +413,6 @@ fn mixed_numeric_helper(op: MixedNumericOp) -> &'static str { MixedNumericOp::Add => "__rt_mixed_numeric_add", MixedNumericOp::Sub => "__rt_mixed_numeric_sub", MixedNumericOp::Mul => "__rt_mixed_numeric_mul", + MixedNumericOp::Pow => "__rt_mixed_numeric_pow", } } diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index f7eeb4a3e0..e13e355788 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -50,9 +50,11 @@ mod type_predicates; pub(crate) mod is_numeric; pub(crate) mod json; pub(crate) mod math; +pub(crate) mod object_props; pub(crate) mod output_buffering; pub(crate) mod pointers; pub(crate) mod regex; +pub(crate) mod round_mode; pub(crate) mod serialize; pub(crate) mod spl; pub(crate) mod system; diff --git a/src/codegen/lower_inst/builtins/arrays.rs b/src/codegen/lower_inst/builtins/arrays.rs index a0c6f9ac38..99d16117c9 100644 --- a/src/codegen/lower_inst/builtins/arrays.rs +++ b/src/codegen/lower_inst/builtins/arrays.rs @@ -26,6 +26,8 @@ use super::super::super::context::FunctionContext; use super::super::callables::runtime_string_descriptor_cases; use super::super::{expect_operand, resolve_int_operand_to_result, store_if_result}; +mod internal_pointer; +mod range_size; mod column; mod key_exists; mod keys; @@ -58,6 +60,7 @@ mod in_array_strings; use map_dispatch::*; use map_results::*; use misc_dispatch::*; +use crate::codegen::lower_inst::receiver_place::ReceiverPlace; use sort_dispatch::*; use type_validation::*; use callback_binding::*; @@ -105,3 +108,789 @@ pub(crate) use callback_builtins::{ lower_in_array, }; pub(super) use in_array_cases::InArrayMode; + +/// How `array_splice()`'s optional `$replacement` argument is handed to the insert helper. +/// +/// PHP casts a non-array `$replacement` to `(array) $replacement`, so a bare scalar inserts one +/// element. `null` and an empty array insert nothing, which is also what an omitted argument does. +enum SpliceReplacement { + /// No `$replacement` argument, or one that is statically known to insert nothing. + Empty, + /// An indexed array whose payload slots are inserted verbatim. + Array(ValueId), + /// An indexed array of typed scalars each boxed into a Mixed cell before insertion, + /// carrying the runtime value_type tag those payloads are boxed with. + BoxedArray(ValueId, u8), + /// An indexed array of boxed Mixed cells read back as plain integers before insertion. + UnboxedArray(ValueId), + /// A single scalar wrapped in a one-element array before the insertion. + /// + /// `scalar_ty` selects the synthesized array's slot width (16 bytes for a string + /// pointer/length pair, 8 for every other scalar). `boxed_tag` is `Some` when the receiver + /// stores boxed Mixed cells, in which case that one-element array goes to the boxing insert + /// helper with the scalar's runtime value_type tag. + Scalar { + value: ValueId, + scalar_ty: PhpType, + boxed_tag: Option, + }, +} + +/// Returns the chunk value type from a key-preserving `array>` result. +/// +/// `array_chunk($a, $n, true)` builds one integer-keyed hash per chunk, so the result element is +/// an `AssocArray` whose keys are the preserved source indices and whose values carry the source +/// element layout the runtime helper copies. +fn array_chunk_result_inner_hash_value_type(result_elem_ty: &PhpType) -> Result { + match result_elem_ty { + PhpType::AssocArray { key, value } if key.codegen_repr() == PhpType::Int => { + Ok(value.codegen_repr()) + } + other => Err(CodegenIrError::unsupported(format!( + "array_chunk preserve_keys result element PHP type {:?}", + other + ))), + } +} + +/// Returns the runtime element tag `__rt_array_count_values` needs for an indexed source. +/// +/// Only `Int`, `Str`, and boxed `Mixed` elements can produce a PHP array key. Every other +/// element type is reported with its own tag so the helper warns and skips each entry the way +/// php-src does, instead of reading a float payload as a pointer. +fn array_count_values_element_tag(source_ty: &PhpType) -> Result { + match source_ty { + PhpType::Array(elem) => runtime_value_tag("array_count_values", &elem.codegen_repr()), + other => Err(CodegenIrError::unsupported(format!( + "array_count_values for PHP type {:?}", + other + ))), + } +} + +/// Returns the indexed-array element type accepted by the `array_reduce()` runtimes. +/// +/// String elements are allowed because `__rt_array_reduce_str` reads the 16-byte +/// `[ptr][len]` payload slots and hands the callback a pointer/length pair; every +/// other accepted element kind is a single 8-byte payload consumed by +/// `__rt_array_reduce`. The accumulator is validated separately and must still fit +/// in one integer register, so no intermediate string ever needs persisting. +fn array_reduce_callback_array_element_type(ty: PhpType) -> Result { + match ty.codegen_repr() { + PhpType::Array(elem) => { + let elem = elem.codegen_repr(); + if elem == PhpType::Str { + return Ok(elem); + } + eight_byte_callback_value_type(elem, "array_reduce") + } + other => Err(CodegenIrError::unsupported(format!( + "array_reduce for PHP type {:?}", + other + ))), + } +} + +/// Returns the `array_reduce()` runtime helper matching the source element width. +fn array_reduce_runtime_label(elem_ty: &PhpType) -> &'static str { + if elem_ty.codegen_repr() == PhpType::Str { + "__rt_array_reduce_str" + } else { + "__rt_array_reduce" + } +} + +/// Reads a literal boolean operand produced by a constant instruction, or `None` when non-literal. +/// +/// Accepts `ConstBool`, integer, float, and null const instructions using PHP truthiness, so any +/// literal flag the frontend folds into an argument slot resolves at compile time. +fn const_bool_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(None); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + match (inst_ref.op, inst_ref.immediate.as_ref()) { + (Op::ConstBool, Some(Immediate::Bool(value))) => Ok(Some(*value)), + (Op::ConstI64, Some(Immediate::I64(value))) => Ok(Some(*value != 0)), + (Op::ConstF64, Some(Immediate::F64(value))) => Ok(Some(*value != 0.0)), + (Op::ConstNull, _) => Ok(Some(false)), + _ => Ok(None), + } +} + +/// Rejects the non-positive `array_chunk()` `$length` reference PHP refuses to chunk with. +/// +/// The chunking helpers advance their cursor by `$length`, so a zero length never reaches the +/// end of the source and kept allocating empty chunks until the heap was exhausted; a negative +/// length walks the cursor backwards. The guard runs while `$length` still sits in its ABI +/// argument register, so it covers every chunk-helper variant at once. +fn emit_array_chunk_length_guard(ctx: &mut FunctionContext<'_>) { + let length_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rsi", + }; + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::SignedAtLeast(length_reg, 1), + ARRAY_CHUNK_NON_POSITIVE_LENGTH_MESSAGE, + ); +} + +/// Writes `$replacement` values into a boxed-Mixed receiver after its removal window closed. +/// +/// The Mixed cell owns the indexed array, so a growth relocation has to be republished into the +/// cell's payload slot rather than into a frame slot. The removed-elements array and the +/// normalized insertion index are parked on the temporary stack across the insertion, which can +/// reach `__rt_array_grow`. +fn emit_mixed_splice_replacement_insert( + ctx: &mut FunctionContext<'_>, + array: ValueId, + replacement: &SpliceReplacement, +) -> Result<()> { + if !replacement.inserts_values() { + return Ok(()); + } + let (removed_reg, at_reg) = splice_result_regs(ctx); + let (_dst_reg, index_reg, replacement_reg) = splice_insert_arg_regs(ctx); + let cell_reg = abi::secondary_scratch_reg(ctx.emitter); + // Temporary layout after both pushes: [0] = replacement array, [16] = removed array, + // [24] = normalized insertion index. + abi::emit_push_reg_pair(ctx.emitter, removed_reg, at_reg); + emit_splice_replacement_pointer(ctx, replacement)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + ctx.load_value_to_reg(array, cell_reg)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldr x0, [x10, #8]"); // read the converted indexed array out of the Mixed cell + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, QWORD PTR [r10 + 8]"); // read the converted indexed array out of the Mixed cell + } + } + abi::emit_load_temporary_stack_slot(ctx.emitter, index_reg, 24); + abi::emit_load_temporary_stack_slot(ctx.emitter, replacement_reg, 0); + emit_splice_boxing_tag(ctx, replacement); + abi::emit_call_label( + ctx.emitter, + array_splice_insert_runtime_helper(replacement, &PhpType::Mixed), + ); + ctx.load_value_to_reg(array, cell_reg)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("str x0, [x10, #8]"); // republish the possibly-relocated indexed array into the Mixed cell + } + Arch::X86_64 => { + ctx.emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // republish the possibly-relocated indexed array into the Mixed cell + } + } + if replacement.owns_temporary_array() { + // `__rt_heap_free` reads its pointer from the INT RESULT register on both targets + // (`x0`/`rax`), not from the first argument register — those differ on x86_64. + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + } + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), 16); + abi::emit_release_temporary_stack(ctx.emitter, 32); + Ok(()) +} + +/// Allocates the one-element replacement shell with the requested payload slot width. +fn emit_splice_one_element_array_new(ctx: &mut FunctionContext<'_>, slot_size: i64) { + let count_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let size_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_int_immediate(ctx.emitter, count_reg, 1); + abi::emit_load_int_immediate(ctx.emitter, size_reg, slot_size); + abi::emit_call_label(ctx.emitter, "__rt_array_new"); +} + +/// Writes `array_splice()`'s `$replacement` values into the gap the removal just opened. +/// +/// Runs directly after the splice helper, whose removed-elements array and normalized insertion +/// index are still live in the result registers; both are parked on the temporary stack because +/// the insertion can reach `__rt_array_grow`. The by-reference receiver's frame slot is refreshed +/// with the possibly-relocated pointer BEFORE the removed array is restored, because that +/// write-back goes through the integer result register. On return the removed array is back in +/// the integer result register, which is what the caller's result normalization reads. +fn emit_splice_replacement_insert( + ctx: &mut FunctionContext<'_>, + array: ValueId, + receiver: ReceiverPlace, + receiver_ty: &PhpType, + replacement: &SpliceReplacement, + elem_ty: &PhpType, +) -> Result<()> { + if !replacement.inserts_values() { + return Ok(()); + } + receiver.require_writable("array_splice replacement")?; + let (removed_reg, at_reg) = splice_result_regs(ctx); + let (dst_reg, index_reg, replacement_reg) = splice_insert_arg_regs(ctx); + // Temporary layout after both pushes: [0] = replacement array, [16] = removed array, + // [24] = normalized insertion index. + abi::emit_push_reg_pair(ctx.emitter, removed_reg, at_reg); + emit_splice_replacement_pointer(ctx, replacement)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + ctx.load_value_to_reg(array, dst_reg)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, index_reg, 24); + abi::emit_load_temporary_stack_slot(ctx.emitter, replacement_reg, 0); + emit_splice_boxing_tag(ctx, replacement); + abi::emit_call_label( + ctx.emitter, + array_splice_insert_runtime_helper(replacement, elem_ty), + ); + ctx.store_result_value(array)?; + if replacement.owns_temporary_array() { + // `__rt_heap_free` reads its pointer from the INT RESULT register on both targets + // (`x0`/`rax`), not from the first argument register — those differ on x86_64. + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + } + receiver.store_back(ctx, array, receiver_ty)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), 16); + abi::emit_release_temporary_stack(ctx.emitter, 32); + Ok(()) +} + +/// Lowers `array_count_values()` through the tally-building runtime helpers. +/// +/// Associative sources take `__rt_hash_count_values`, which dispatches on each entry's +/// RUNTIME value tag. Indexed sources take `__rt_array_count_values` with the COMPILE-TIME +/// element tag, because an indexed array's payload carries no per-slot tag: the tag selects +/// between the 8-byte integer slot layout, the 16-byte string slot layout, and the boxed +/// `Mixed` pointer layout. Any other element tag makes every entry skippable, which is exactly +/// php-src's behaviour for a `float`/`bool`/array/object element. + + +/// Lowers `ArrayPtrKey` through the internal-array-pointer backend. +pub(crate) fn lower_array_ptr_key(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + internal_pointer::lower_array_ptr_key(ctx, inst) +} + +/// Lowers `ArrayPtrSeek` through the internal-array-pointer backend. +pub(crate) fn lower_array_ptr_seek( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + internal_pointer::lower_array_ptr_seek(ctx, inst) +} + +/// Lowers `ArrayPtrValue` through the internal-array-pointer backend. +pub(crate) fn lower_array_ptr_value( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + internal_pointer::lower_array_ptr_value(ctx, inst) +} + +/// php-src's verbatim `ValueError` wording for `array_chunk()` with a non-positive `$length`. +const ARRAY_CHUNK_NON_POSITIVE_LENGTH_MESSAGE: &str = + "array_chunk(): Argument #2 ($length) must be greater than 0"; + +/// Returns the helper that inserts `$replacement` values with the right ownership handling. +fn array_splice_insert_runtime_helper( + replacement: &SpliceReplacement, + elem_ty: &PhpType, +) -> &'static str { + if replacement.boxing_tag().is_some() { + return "__rt_array_splice_insert_boxed"; + } + if matches!(replacement, SpliceReplacement::UnboxedArray(_)) { + return "__rt_array_splice_insert_unboxed"; + } + if elem_ty.codegen_repr() == PhpType::Str { + return "__rt_array_splice_insert_str"; + } + if elem_ty.is_refcounted() { + "__rt_array_splice_insert_refcounted" + } else { + "__rt_array_splice_insert" + } +} + +/// Materializes the extra value_type-tag argument the boxing insert helper reads. +fn emit_splice_boxing_tag(ctx: &mut FunctionContext<'_>, replacement: &SpliceReplacement) { + let Some(tag) = replacement.boxing_tag() else { + return; + }; + let reg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_int_immediate(ctx.emitter, reg, i64::from(tag)); +} + +/// Materializes the replacement's indexed-array pointer into the integer result register. +/// +/// An array argument is loaded directly. A bare scalar is wrapped in a fresh one-element array +/// whose payload slot holds the value; the insert helpers persist or retain what they insert, so +/// the caller frees that temporary shell afterwards without touching the value itself. +fn emit_splice_replacement_pointer( + ctx: &mut FunctionContext<'_>, + replacement: &SpliceReplacement, +) -> Result<()> { + let result_reg = abi::int_result_reg(ctx.emitter); + match replacement { + SpliceReplacement::Empty => { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + Ok(()) + } + SpliceReplacement::Array(value) + | SpliceReplacement::BoxedArray(value, _) + | SpliceReplacement::UnboxedArray(value) => { + ctx.load_value_to_reg(*value, result_reg)?; + Ok(()) + } + SpliceReplacement::Scalar { + value, scalar_ty, .. + } if scalar_ty.codegen_repr() == PhpType::Str => { + emit_splice_one_element_string_array(ctx, *value) + } + SpliceReplacement::Scalar { value, .. } => { + let scratch = abi::secondary_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(*value, scratch)?; + abi::emit_push_reg(ctx.emitter, scratch); + emit_splice_one_element_array_new(ctx, 8); + abi::emit_pop_reg(ctx.emitter, scratch); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("str x10, [x0, #24]"); // store the scalar replacement into the one-element array + ctx.emitter.instruction("mov x10, #1"); // the synthesized replacement array holds exactly one element + ctx.emitter.instruction("str x10, [x0]"); // publish the one-element logical length + } + Arch::X86_64 => { + ctx.emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the scalar replacement into the one-element array + ctx.emitter.instruction("mov r10, 1"); // the synthesized replacement array holds exactly one element + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // publish the one-element logical length + } + } + Ok(()) + } + } +} + +/// Lowers `array_count_values()` through the tally-building runtime helpers. +/// +/// Associative sources take `__rt_hash_count_values`, which dispatches on each entry's +/// RUNTIME value tag. Indexed sources take `__rt_array_count_values` with the COMPILE-TIME +/// element tag, because an indexed array's payload carries no per-slot tag: the tag selects +/// between the 8-byte integer slot layout, the 16-byte string slot layout, and the boxed +/// `Mixed` pointer layout. Any other element tag makes every entry skippable, which is exactly +/// php-src's behaviour for a `float`/`bool`/array/object element. +pub(crate) fn lower_array_count_values( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "array_count_values", 1)?; + let array = expect_operand(inst, 0)?; + let source_ty = ctx.value_php_type(array)?.codegen_repr(); + if matches!(source_ty, PhpType::AssocArray { .. }) { + ctx.load_value_to_result(array)?; + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rdi, rax"); // pass the source hash pointer as the tally helper argument + } + abi::emit_call_label(ctx.emitter, "__rt_hash_count_values"); + return store_if_result(ctx, inst); + } + let element_tag = array_count_values_element_tag(&source_ty)?; + ctx.load_value_to_result(array)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("mov x1, #{}", element_tag)); // pass the compile-time element tag to the tally helper + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the source indexed-array pointer as the tally helper argument + ctx.emitter + .instruction(&format!("mov rsi, {}", element_tag)); // pass the compile-time element tag to the tally helper + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_count_values"); + store_if_result(ctx, inst) +} + +/// Lowers `array_slice($array, $offset, $length, true)` into an owned integer-keyed hash. +/// +/// The runtime helper normalizes the PHP window through the same `emit_slice_bounds` prologue as +/// `__rt_array_slice`, then inserts each selected element at its ORIGINAL index, persisting +/// strings and retaining heap payloads, so the result is a freshly owned hash whose keys match +/// PHP's `preserve_keys` output exactly. The checker types this call as +/// `AssocArray { key: Int, value: T }`, which is re-verified here. +fn lower_array_slice_preserve_keys( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + array: ValueId, +) -> Result<()> { + let PhpType::Array(_) = ctx.value_php_type(array)?.codegen_repr() else { + return Err(CodegenIrError::unsupported(format!( + "array_slice preserve_keys for PHP type {:?}", + ctx.value_php_type(array)? + ))); + }; + let PhpType::AssocArray { .. } = inst.result_php_type.codegen_repr() else { + return Err(CodegenIrError::unsupported(format!( + "array_slice preserve_keys result PHP type {:?}", + inst.result_php_type + ))); + }; + let offset = expect_operand(inst, 1)?; + let length = slice_like_length_operand(inst)?; + lower_slice_like_args(ctx, array, offset, length, "array_slice")?; + abi::emit_call_label(ctx.emitter, "__rt_array_slice_to_hash"); + store_if_result(ctx, inst) +} + +/// Calls one of the `__rt_hash_*sort` insertion-order relinking helpers. +/// +/// The receiver is split with `__rt_hash_ensure_unique` first, so an aliased copy taken +/// before the call keeps the original iteration order, and the possibly relocated pointer +/// is written back to the source local before the sorter runs. The helpers only rewrite +/// the table's `prev`/`next`/`head`/`tail` links, so no key or value changes ownership. +fn lower_hash_link_sort( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + helper: &str, +) -> Result<()> { + let array = expect_operand(inst, 0)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; + ensure_unique_hash_sort_source(ctx, array)?; + receiver.store_back_value(ctx, array)?; + let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + ctx.load_value_to_reg(array, array_arg_reg)?; + abi::emit_call_label(ctx.emitter, helper); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 0x7fff_ffff_ffff_fffe, + ); + store_if_result(ctx, inst) +} + +/// Resolves the `array_slice`/`array_splice` length-present flag into the integer result register. +/// +/// PHP's `?int $length` treats `null` as "to the end of the array", and every other `i64` — including +/// `-1` — is a real length, so the runtime helpers cannot recognise "no length" from the length value +/// itself. The flag is therefore materialized separately: an omitted or statically `Void` argument is +/// the immediate `0`, a statically typed integer is the immediate `1`, and a boxed `Mixed` argument is +/// unboxed at runtime so a `null` payload (runtime tag 8) also reports `0`. +fn resolve_slice_length_present_to_result( + ctx: &mut FunctionContext<'_>, + length: Option, +) -> Result<()> { + let reg = abi::int_result_reg(ctx.emitter); + if slice_length_is_statically_absent(ctx, length)? { + abi::emit_load_int_immediate(ctx.emitter, reg, 0); + return Ok(()); + } + let length = length.expect("length present"); + if !matches!( + ctx.value_php_type(length)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { + abi::emit_load_int_immediate(ctx.emitter, reg, 1); + return Ok(()); + } + ctx.load_value_to_result(length)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #8"); // runtime tag 8 marks a boxed PHP null length argument + ctx.emitter.instruction("cset x0, ne"); // report a length only when the boxed payload is not null + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 8"); // runtime tag 8 marks a boxed PHP null length argument + ctx.emitter.instruction("setne al"); // report a length only when the boxed payload is not null + ctx.emitter.instruction("movzx rax, al"); // widen the length-present flag to a full integer argument word + } + } + Ok(()) +} + +/// Reports whether the `array_slice`/`array_splice` length argument is absent at compile time. +/// +/// A missing operand and a statically `Void` operand both mean the PHP call omitted `$length` (or +/// passed a literal `null`), which selects the "slice to the end of the array" behavior. +fn slice_length_is_statically_absent( + ctx: &mut FunctionContext<'_>, + length: Option, +) -> Result { + match length { + None => Ok(true), + Some(length) => Ok(matches!( + ctx.value_php_type(length)?.codegen_repr(), + PhpType::Void + )), + } +} + +/// Returns the `$length` operand of a slice-like call, or `None` when the argument was omitted. +/// +/// PHP's `$length` is the third parameter, so a call that also passes `$preserve_keys` always +/// materializes it — the argument planner fills the gap with the parameter's `null` default. +fn slice_like_length_operand(inst: &Instruction) -> Result> { + if inst.operands.len() >= 3 { + return Ok(Some(expect_operand(inst, 2)?)); + } + Ok(None) +} + +/// Reads the literal `$preserve_keys` flag of a slice-like call. +/// +/// The checker rejects a non-literal flag because it decides the result's static shape, so a +/// non-literal operand here can only mean the checker and the backend disagree about this call. +fn slice_like_preserve_keys( + ctx: &FunctionContext<'_>, + inst: &Instruction, + name: &str, +) -> Result { + match inst.operands.get(3).copied() { + None => Ok(false), + Some(flag) => const_bool_operand(ctx, flag)?.ok_or_else(|| { + CodegenIrError::unsupported(format!( + "{} preserve_keys argument that is not a compile-time literal", + name + )) + }), + } +} + +/// Reports whether a mutating array builtin's first operand is a hash-backed array. +fn sort_receiver_is_hash(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result { + let array = expect_operand(inst, 0)?; + Ok(matches!( + ctx.value_php_type(array)?.codegen_repr(), + PhpType::AssocArray { .. } + )) +} + +/// The `__rt_array_splice_insert*` argument registers: destination, index, replacement. +fn splice_insert_arg_regs( + ctx: &FunctionContext<'_>, +) -> (&'static str, &'static str, &'static str) { + match ctx.emitter.target.arch { + Arch::AArch64 => ("x0", "x1", "x2"), + Arch::X86_64 => ("rdi", "rsi", "rdx"), + } +} + +/// The registers `__rt_array_splice*` leaves the removed array and the insertion index in. +fn splice_result_regs(ctx: &FunctionContext<'_>) -> (&'static str, &'static str) { + match ctx.emitter.target.arch { + Arch::AArch64 => ("x0", "x1"), + Arch::X86_64 => ("rax", "rdx"), + } +} + +/// Wraps a bare string replacement in a one-element 16-byte-slot indexed array. +/// +/// The shell holds the caller's borrowed pointer/length pair: `__rt_array_splice_insert_str` +/// duplicates it with `__rt_str_persist` and `__rt_array_splice_insert_boxed` persists it into a +/// Mixed cell, so freeing the shell afterwards never touches the string bytes. +fn emit_splice_one_element_string_array( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + // The pointer/length pair only both land in the canonical string result registers through + // the result loader; a single-register load leaves the length slot holding stale bytes. + ctx.load_value_to_result(value)?; + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + emit_splice_one_element_array_new(ctx, 16); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("ldp x10, x11, [sp], #16"); // pop the borrowed string pointer/length pair after the constructor + ctx.emitter.instruction("stp x10, x11, [x0, #24]"); // store the borrowed pair into the one-element string array + ctx.emitter.instruction("mov x10, #1"); // the synthesized replacement array holds exactly one element + ctx.emitter.instruction("str x10, [x0]"); // publish the one-element logical length + } + Arch::X86_64 => { + ctx.emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the borrowed string pointer after the constructor + ctx.emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // reload the borrowed string length after the constructor + ctx.emitter.instruction("add rsp, 16"); // release the borrowed string pointer/length staging slot + ctx.emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the borrowed string pointer into the one-element string array + ctx.emitter.instruction("mov QWORD PTR [rax + 32], r11"); // store the borrowed string length into the one-element string array + ctx.emitter.instruction("mov r10, 1"); // the synthesized replacement array holds exactly one element + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // publish the one-element logical length + } + } + Ok(()) +} + +impl SpliceReplacement { + /// Classifies the `$replacement` operand against the receiver's element type. + /// + /// Four shapes are accepted, in this order: an indexed array whose element type already + /// matches the receiver's payload slots; an indexed array of typed scalars going into a + /// heterogeneous `array` receiver, whose values are boxed one at a time; an indexed + /// array of boxed `Mixed` cells going into an `array`/`array` receiver, which is + /// what an overflow-checked expression such as `[$x + 1]` produces; and a bare non-refcounted + /// scalar of the receiver's element type, which PHP casts to a one-element array. Anything + /// else — an array the backend cannot re-represent in the receiver's slots, a bare boxed + /// `Mixed` value, a `Str` receiver (whose payload slots are wider than the splice helpers + /// move) — is an explicit `unsupported` diagnostic rather than a mistyped insertion. + fn resolve( + ctx: &mut FunctionContext<'_>, + replacement: Option, + elem_ty: &PhpType, + ) -> Result { + let Some(replacement) = replacement else { + return Ok(Self::Empty); + }; + let replacement_ty = ctx.value_php_type(replacement)?.codegen_repr(); + if matches!(replacement_ty, PhpType::Void | PhpType::Never) { + // A literal `null` replacement, or the registry's own `[]` default. + return Ok(Self::Empty); + } + if let PhpType::Array(inner) = &replacement_ty { + let inner = inner.codegen_repr(); + if matches!(inner, PhpType::Void | PhpType::Never) { + return Ok(Self::Empty); + } + if &inner == elem_ty && splice_insert_slot_is_supported(elem_ty) { + return Ok(Self::Array(replacement)); + } + // A heterogeneous receiver stores boxed Mixed cells, so a typed scalar replacement + // has to be boxed element by element before it lands in the payload. + if elem_ty == &PhpType::Mixed && splice_boxable_scalar_slot(&inner) { + let tag = runtime_value_tag("array_splice", &inner)?; + return Ok(Self::BoxedArray(replacement, tag)); + } + // The mirror case: an overflow-checked expression boxes its result, so + // `[$x + 1, $x + 2]` is an `array` even for an `array` receiver. Read + // each cell back as a plain integer instead of storing the cell pointer. + if inner == PhpType::Mixed && matches!(elem_ty, PhpType::Int | PhpType::Bool) { + return Ok(Self::UnboxedArray(replacement)); + } + } + if &replacement_ty == elem_ty + && splice_insert_slot_is_supported(elem_ty) + && !elem_ty.is_refcounted() + { + return Ok(Self::Scalar { + value: replacement, + scalar_ty: replacement_ty, + boxed_tag: None, + }); + } + // A bare scalar into a heterogeneous receiver: PHP casts it to a one-element array, and + // that element has to be boxed exactly like an array replacement's elements are. + if elem_ty == &PhpType::Mixed && splice_boxable_scalar_slot(&replacement_ty) { + let tag = runtime_value_tag("array_splice", &replacement_ty)?; + return Ok(Self::Scalar { + value: replacement, + scalar_ty: replacement_ty, + boxed_tag: Some(tag), + }); + } + Err(CodegenIrError::unsupported(format!( + "array_splice replacement PHP type {:?} for indexed-array element PHP type {:?}. \ + PHP would make the receiver heterogeneous, which needs an `array` receiver \ + slot; a by-reference parameter, a `&$x` binding, and an object/static property \ + receiver all share their storage with a slot this call cannot retype", + replacement_ty, elem_ty + ))) + } + + /// Reports whether any element has to be written into the removal gap at run time. + fn inserts_values(&self) -> bool { + !matches!(self, Self::Empty) + } + + /// Reports whether the helper receives a one-element array this lowering allocated. + fn owns_temporary_array(&self) -> bool { + matches!(self, Self::Scalar { .. }) + } + + /// Returns the runtime value_type tag the boxing insert helper reads, when boxing applies. + fn boxing_tag(&self) -> Option { + match self { + Self::BoxedArray(_, tag) => Some(*tag), + Self::Scalar { boxed_tag, .. } => *boxed_tag, + _ => None, + } + } +} + +/// Reports whether a replacement element type can be boxed one slot at a time into a Mixed cell. +/// +/// `__rt_mixed_from_value` stores the raw payload word without retaining it, so only the +/// non-refcounted scalars whose slot IS the value qualify. `Str` qualifies too: the boxing helper +/// reads its wider pointer/length slot and `__rt_mixed_from_value` persists the bytes itself, so +/// the Mixed cell owns storage the replacement array still holds independently. +fn splice_boxable_scalar_slot(elem_ty: &PhpType) -> bool { + matches!( + elem_ty, + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) +} + +/// Reports whether the splice insert helpers can move this element type's payload slots. +/// +/// Every scalar and refcounted element representation qualifies. `Str` reaches a dedicated +/// helper rather than the shared 8-byte one, because indexed string arrays store 16-byte +/// `{pointer, length}` slots that must not be moved eight bytes at a time. +fn splice_insert_slot_is_supported(elem_ty: &PhpType) -> bool { + matches!( + elem_ty, + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Callable + ) || elem_ty.is_refcounted() +} + +/// php-src's verbatim `ValueError` wording for a negative `range()` `$step` on an increasing range. +const RANGE_NEGATIVE_STEP_MESSAGE: &str = + "range(): Argument #3 ($step) must be greater than 0 for increasing ranges"; + +/// php-src's verbatim `ValueError` wording for a `range()` `$step` wider than the spanned interval. +const RANGE_STEP_TOO_WIDE_MESSAGE: &str = "range(): Argument #3 ($step) must be less than the range spanned by argument #1 ($start) and argument #2 ($end)"; + +/// php-src's verbatim `ValueError` wording for `range()` with a zero `$step`. +const RANGE_ZERO_STEP_MESSAGE: &str = "range(): Argument #3 ($step) cannot be 0"; + +/// Returns whether `array_search(..., strict: true)` can never match for these static types. +/// +/// PHP's `===` requires identical types, so a scalar needle can never match an element of a +/// different scalar type (`array_search(1, [true, false], true)` is `false` while the loose +/// form finds index 0). A boxed `Mixed` element type carries its type tag at runtime and is +/// therefore never statically impossible. +fn array_search_strict_never_matches(needle_ty: &PhpType, array_ty: &PhpType) -> bool { + let needle_ty = needle_ty.clone().codegen_repr(); + let element_ty = match array_ty.clone().codegen_repr() { + PhpType::Array(elem) => elem.codegen_repr(), + PhpType::AssocArray { value, .. } => value.codegen_repr(), + _ => return false, + }; + matches!(needle_ty, PhpType::Int | PhpType::Bool | PhpType::Str) + && matches!(element_ty, PhpType::Int | PhpType::Bool | PhpType::Str) + && needle_ty != element_ty +} + +/// Emits `array_search()`'s ordinary (non-strict-impossible) element scan. +/// +/// Leaves the boxed `int|string|false` result in the integer result register for the caller's +/// `store_if_result`, dispatching between the associative, empty, scalar, and string paths. +fn lower_array_search_loose( + ctx: &mut FunctionContext<'_>, + needle: ValueId, + array: ValueId, + needle_ty: PhpType, + array_ty: PhpType, +) -> Result<()> { + if search::try_lower_assoc_array_search( + ctx, + needle, + array, + needle_ty.clone(), + array_ty.clone(), + )? { + return Ok(()); + } + match supported_array_search_case(needle_ty, array_ty)? { + ArraySearchCase::Empty => box_array_search_miss(ctx), + ArraySearchCase::Scalar => lower_array_search_scalar(ctx, needle, array)?, + ArraySearchCase::String => lower_array_search_string(ctx, needle, array)?, + } + Ok(()) +} diff --git a/src/codegen/lower_inst/builtins/arrays/basic.rs b/src/codegen/lower_inst/builtins/arrays/basic.rs index 21c775b660..0aa4ff9bb2 100644 --- a/src/codegen/lower_inst/builtins/arrays/basic.rs +++ b/src/codegen/lower_inst/builtins/arrays/basic.rs @@ -79,16 +79,39 @@ pub(crate) fn lower_array_push(ctx: &mut FunctionContext<'_>, inst: &Instruction } /// Lowers `array_chunk()` by splitting an indexed array into nested indexed arrays. +/// +/// PHP's `bool $preserve_keys = false` keeps each chunk's source integer keys instead of +/// renumbering it from zero. A dense indexed array cannot hold a window that does not start at +/// key 0, so the key-preserving form lowers to `__rt_array_chunk_to_hash`, which builds one owned +/// hash per chunk. The checker guarantees the flag is a literal (it decides the result's static +/// shape), so a non-literal operand can only mean the checker and the backend disagree. pub(crate) fn lower_array_chunk(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::super::ensure_arg_count(inst, "array_chunk", 2)?; + ensure_arg_count_between(inst, "array_chunk", 2, 3)?; let array = expect_operand(inst, 0)?; let length = expect_operand(inst, 1)?; + let preserve_keys = match inst.operands.get(2).copied() { + None => false, + Some(flag) => const_bool_operand(ctx, flag)?.ok_or_else(|| { + CodegenIrError::unsupported( + "array_chunk preserve_keys argument that is not a compile-time literal".to_string(), + ) + })?, + }; let source_elem_ty = array_chunk_source_element_type(ctx.value_php_type(array)?)?; let result_elem_ty = result_array_element_type("array_chunk", &inst.result_php_type.codegen_repr())?; - let result_inner_elem_ty = array_chunk_result_inner_element_type(&result_elem_ty)?; + let result_inner_elem_ty = if preserve_keys { + array_chunk_result_inner_hash_value_type(&result_elem_ty)? + } else { + array_chunk_result_inner_element_type(&result_elem_ty)? + }; require_array_chunk_result_type(&source_elem_ty, &result_inner_elem_ty)?; - lower_array_chunk_call(ctx, array, length, &source_elem_ty)?; + let runtime_label = if preserve_keys { + "__rt_array_chunk_to_hash" + } else { + array_chunk_runtime_helper(&source_elem_ty) + }; + lower_array_chunk_call(ctx, array, length, runtime_label)?; crate::codegen::emit_array_value_type_stamp( ctx.emitter, abi::int_result_reg(ctx.emitter), @@ -275,9 +298,28 @@ pub(super) fn hash_flip_result_value_type(result_ty: &PhpType) -> Result, inst: &Instruction) -> Result<()> { - super::super::ensure_arg_count(inst, "array_reverse", 1)?; + ensure_arg_count_between(inst, "array_reverse", 1, 2)?; let array = expect_operand(inst, 0)?; + let preserve_keys = match inst.operands.get(1).copied() { + None => false, + Some(flag) => const_bool_operand(ctx, flag)?.ok_or_else(|| { + CodegenIrError::unsupported( + "array_reverse preserve_keys argument that is not a compile-time literal" + .to_string(), + ) + })?, + }; + if preserve_keys { + return lower_array_reverse_preserve_keys(ctx, inst, array); + } let elem_ty = eight_byte_indexed_array_element_type(ctx.value_php_type(array)?, "array_reverse")?; ctx.load_value_to_result(array)?; @@ -302,3 +344,61 @@ pub(crate) fn lower_array_unique(ctx: &mut FunctionContext<'_>, inst: &Instructi store_if_result(ctx, inst) } + + +/// Lowers `array_reverse($array, true)` into an owned integer-keyed hash. +/// +/// The runtime helper walks the source payload from the last slot to the first and inserts each +/// element at its ORIGINAL index, persisting strings and retaining heap payloads, so the result +/// is a freshly owned hash whose keys match PHP's `preserve_keys` output exactly. The checker +/// types this call as `AssocArray { key: Int, value: T }`, which is re-verified here. +fn lower_array_reverse_preserve_keys( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + array: ValueId, +) -> Result<()> { + let PhpType::Array(_) = ctx.value_php_type(array)?.codegen_repr() else { + return Err(CodegenIrError::unsupported(format!( + "array_reverse preserve_keys for PHP type {:?}", + ctx.value_php_type(array)? + ))); + }; + let PhpType::AssocArray { .. } = inst.result_php_type.codegen_repr() else { + return Err(CodegenIrError::unsupported(format!( + "array_reverse preserve_keys result PHP type {:?}", + inst.result_php_type + ))); + }; + ctx.load_value_to_result(array)?; + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rdi, rax"); // pass the source indexed-array pointer as the key-preserving reverse helper argument + } + abi::emit_call_label(ctx.emitter, "__rt_array_to_hash_reverse"); + store_if_result(ctx, inst) +} + +/// Reads a literal boolean operand produced by a constant instruction, or `None` when non-literal. +/// +/// Accepts `ConstBool`, integer, float, and null const instructions using PHP truthiness, so any +/// literal flag the frontend folds into an argument slot resolves at compile time. +fn const_bool_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(None); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + match (inst_ref.op, inst_ref.immediate.as_ref()) { + (Op::ConstBool, Some(Immediate::Bool(value))) => Ok(Some(*value)), + (Op::ConstI64, Some(Immediate::I64(value))) => Ok(Some(*value != 0)), + (Op::ConstF64, Some(Immediate::F64(value))) => Ok(Some(*value != 0.0)), + (Op::ConstNull, _) => Ok(Some(false)), + _ => Ok(None), + } +} + diff --git a/src/codegen/lower_inst/builtins/arrays/callback_builtins.rs b/src/codegen/lower_inst/builtins/arrays/callback_builtins.rs index c1563562b4..fce3181cf0 100644 --- a/src/codegen/lower_inst/builtins/arrays/callback_builtins.rs +++ b/src/codegen/lower_inst/builtins/arrays/callback_builtins.rs @@ -8,6 +8,7 @@ //! - Preserves callback ABI, target parity, array storage, and ownership contracts. use super::*; +use crate::codegen::lower_inst::receiver_place::ReceiverPlace; /// Returns the scalar callback element type for an indexed-array predicate/comparator builtin. /// @@ -341,16 +342,12 @@ pub(crate) fn lower_array_multisort( eight_byte_indexed_array_element_type(ctx.value_php_type(arr2)?, "array_multisort")?; // -- copy-on-write split both by-ref arrays and publish the new pointers to their locals -- - let slot1 = source_load_local_slot(ctx, arr1)?; + let receiver1 = ReceiverPlace::resolve(ctx, arr1)?; ensure_unique_sort_source(ctx, arr1)?; - if let Some(slot) = slot1 { - ctx.store_value_to_local(slot, arr1)?; - } - let slot2 = source_load_local_slot(ctx, arr2)?; + receiver1.store_back_value(ctx, arr1)?; + let receiver2 = ReceiverPlace::resolve(ctx, arr2)?; ensure_unique_sort_source(ctx, arr2)?; - if let Some(slot) = slot2 { - ctx.store_value_to_local(slot, arr2)?; - } + receiver2.store_back_value(ctx, arr2)?; match ctx.emitter.target.arch { Arch::AArch64 => { @@ -372,26 +369,34 @@ pub(crate) fn lower_array_multisort( } /// Lowers `array_search()` for indexed arrays with integer-like payloads. +/// +/// PHP's third parameter (`bool $strict = false`) selects `===` instead of `==`. Every +/// comparison this emitter can already lower is value-exact, so the two modes only diverge +/// when the needle and the element type are statically different scalar types — the case +/// `array_search_strict_never_matches()` detects. There, the strict answer is unconditionally +/// `false`, so the flag is resolved with a runtime branch around the ordinary search. pub(crate) fn lower_array_search(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::super::ensure_arg_count(inst, "array_search", 2)?; + ensure_arg_count_between(inst, "array_search", 2, 3)?; let needle = expect_operand(inst, 0)?; let array = expect_operand(inst, 1)?; let needle_ty = ctx.value_php_type(needle)?; let array_ty = ctx.value_php_type(array)?; - if search::try_lower_assoc_array_search( - ctx, - needle, - array, - needle_ty.clone(), - array_ty.clone(), - )? { - store_if_result(ctx, inst)?; - return Ok(()); - } - match supported_array_search_case(needle_ty, array_ty)? { - ArraySearchCase::Empty => box_array_search_miss(ctx), - ArraySearchCase::Scalar => lower_array_search_scalar(ctx, needle, array)?, - ArraySearchCase::String => lower_array_search_string(ctx, needle, array)?, + let strict = inst.operands.get(2).copied(); + match strict { + Some(strict) if array_search_strict_never_matches(&needle_ty, &array_ty) => { + let strict_label = ctx.next_label("array_search_strict"); + let done_label = ctx.next_label("array_search_strict_done"); + branch_if_bool_value_true(ctx, strict, &strict_label)?; + lower_array_search_loose(ctx, needle, array, needle_ty, array_ty)?; + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&strict_label); + box_array_search_miss(ctx); + ctx.emitter.label(&done_label); + } + // Either `strict` was omitted, or the needle and element types agree (or one side is + // a boxed `Mixed` compared tag-exactly), in which case `===` and `==` pick the same + // element and the flag has no observable effect on the emitted search. + _ => lower_array_search_loose(ctx, needle, array, needle_ty, array_ty)?, } store_if_result(ctx, inst) } diff --git a/src/codegen/lower_inst/builtins/arrays/callback_targets.rs b/src/codegen/lower_inst/builtins/arrays/callback_targets.rs index 1e5b5acafe..96cbcb795c 100644 --- a/src/codegen/lower_inst/builtins/arrays/callback_targets.rs +++ b/src/codegen/lower_inst/builtins/arrays/callback_targets.rs @@ -306,6 +306,12 @@ pub(super) fn instance_method_already_emitted( } /// Verifies the wrapper can forward the callback argument ABI without boxing or shuffling pairs. +/// +/// String arguments occupy two integer ABI slots, so they are only forwarded when every +/// visible argument is a string: that is the shape the runtime callback helpers actually +/// produce (a single-string element callback, or the two-string `usort()` comparator). +/// A mixed string/scalar list would need a register-shuffling wrapper no runtime helper +/// currently calls, so it stays a diagnosed unsupported feature. pub(super) fn require_static_method_callback_arg_types( owner: &str, callback_name: &str, @@ -324,9 +330,12 @@ pub(super) fn require_static_method_callback_arg_types( .any(|ty| matches!(ty.codegen_repr(), PhpType::Str)) && !(visible_arg_types.len() == 1 && matches!(visible_arg_types[0].codegen_repr(), PhpType::Str)) + && !visible_arg_types + .iter() + .all(|ty| matches!(ty.codegen_repr(), PhpType::Str)) { return Err(CodegenIrError::unsupported(format!( - "{} '{}' with string callback args outside the one-argument ABI", + "{} '{}' with mixed string and scalar callback args", owner, callback_name ))); } diff --git a/src/codegen/lower_inst/builtins/arrays/fill_helpers.rs b/src/codegen/lower_inst/builtins/arrays/fill_helpers.rs index d732b5d717..6242fd83ca 100644 --- a/src/codegen/lower_inst/builtins/arrays/fill_helpers.rs +++ b/src/codegen/lower_inst/builtins/arrays/fill_helpers.rs @@ -33,6 +33,7 @@ pub(super) fn lower_array_fill_call( ctx.load_string_value_to_regs(value, "rsi", "rdx")?; } } + emit_array_fill_count_guard(ctx, false); abi::emit_call_label(ctx.emitter, array_fill_runtime_helper(value_ty)); return Ok(()); } @@ -48,10 +49,57 @@ pub(super) fn lower_array_fill_call( ctx.load_value_to_reg(value, "rdx")?; } } + emit_array_fill_count_guard(ctx, true); abi::emit_call_label(ctx.emitter, array_fill_runtime_helper(value_ty)); Ok(()) } +/// php-src's verbatim `ValueError` wording for `array_fill()` with a negative `$count`. +const ARRAY_FILL_NEGATIVE_COUNT_MESSAGE: &str = + "array_fill(): Argument #2 ($count) must be greater than or equal to 0"; + +/// The largest `array_fill()` `$count` reference PHP will even attempt to build an array for. +/// +/// php-src bounds the count against `INT_MAX` — not against the maximum array size — before it +/// reaches the allocator, so `array_fill(0, 2147483647, …)` is accepted (and then fails on +/// memory) while `array_fill(0, 2147483648, …)` is a `ValueError` for every `$start` and value. +const ARRAY_FILL_MAX_COUNT: i64 = 2_147_483_647; + +/// php-src's verbatim `ValueError` wording for an oversized `array_fill()` `$count`. +const ARRAY_FILL_COUNT_TOO_LARGE_MESSAGE: &str = "array_fill(): Argument #2 ($count) is too large"; + +/// Rejects the `array_fill()` `$count` values reference PHP refuses to build an array for. +/// +/// The fill helpers write `$count` straight into the array header's length field without +/// clamping it, so a negative count produced an array whose header claimed a negative length +/// — `count()` answered `-1` and every walk over it read past the payload. A count past +/// `INT_MAX` is memory-safe today (the allocation guards catch it) but reported the process's +/// uncatchable heap fatal where reference PHP throws, so `try { … } catch (ValueError $e)` +/// could never see it; bounding the argument here raises PHP's own error instead. +/// `second_arg_reg` selects which ABI register currently holds `$count`: the string fill helper +/// takes `(count, ptr, len)`, every other fill helper takes `(start, count, value)`. +fn emit_array_fill_count_guard(ctx: &mut FunctionContext<'_>, second_arg_reg: bool) { + let count_reg = match (ctx.emitter.target.arch, second_arg_reg) { + (Arch::AArch64, false) => "x0", + (Arch::AArch64, true) => "x1", + (Arch::X86_64, false) => "rdi", + (Arch::X86_64, true) => "rsi", + }; + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::SignedAtLeast(count_reg, 0), + ARRAY_FILL_NEGATIVE_COUNT_MESSAGE, + ); + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::SignedAtMost( + count_reg, + ARRAY_FILL_MAX_COUNT, + ), + ARRAY_FILL_COUNT_TOO_LARGE_MESSAGE, + ); +} + /// Calls the keyed `array_fill()` runtime helper after materializing the boxed payload fields. pub(super) fn lower_array_fill_assoc_call( ctx: &mut FunctionContext<'_>, @@ -75,6 +123,7 @@ pub(super) fn lower_array_fill_assoc_call( abi::emit_load_int_immediate(ctx.emitter, "r8", value_tag); } } + emit_array_fill_count_guard(ctx, true); abi::emit_call_label(ctx.emitter, "__rt_array_fill_assoc"); Ok(()) } diff --git a/src/codegen/lower_inst/builtins/arrays/filter.rs b/src/codegen/lower_inst/builtins/arrays/filter.rs index 14056ad6a8..9f3b100a24 100644 --- a/src/codegen/lower_inst/builtins/arrays/filter.rs +++ b/src/codegen/lower_inst/builtins/arrays/filter.rs @@ -114,4 +114,3 @@ pub(crate) fn lower_array_filter(ctx: &mut FunctionContext<'_>, inst: &Instructi } store_if_result(ctx, inst) } - diff --git a/src/codegen/lower_inst/builtins/arrays/internal_pointer.rs b/src/codegen/lower_inst/builtins/arrays/internal_pointer.rs new file mode 100644 index 0000000000..02289531d7 --- /dev/null +++ b/src/codegen/lower_inst/builtins/arrays/internal_pointer.rs @@ -0,0 +1,97 @@ +//! Purpose: +//! Lowers the three typed internal-array-pointer runtime targets (`ArrayPtrSeek`, +//! `ArrayPtrKey`, `ArrayPtrValue`) that back PHP's `key`/`current`/`next`/`prev`/ +//! `reset`/`end`. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::arrays::lower_array_ptr_*()` through the +//! typed runtime-function dispatch groups. +//! +//! Key details: +//! - Each target is a thin, target-agnostic argument marshal plus one call into an +//! `__rt_array_ptr_*` helper; every supported target shares the same path because the +//! register choices come from `abi::int_arg_reg_name`. +//! - Operand counts are enforced here rather than through a registry runtime signature: +//! the PHP builtins declare one argument while these calls carry the cursor (and, for +//! a seek, the seek mode) as extra operands, so a call that reached this code with the +//! plain PHP arity is a lowering bug and must fail loudly instead of reading garbage. + +use crate::codegen::abi; +use crate::codegen::context::FunctionContext; +use crate::codegen::{CodegenIrError, Result}; +use crate::ir::Instruction; + +use super::super::super::{expect_operand, store_if_result}; + +/// Lowers `ArrayPtrSeek`: `(container, cursor, mode) -> new cursor`. +/// +/// The helper owns all cursor arithmetic and the bounds checks against the live element +/// count, so the backend only marshals three integer-class operands and calls it. +pub(super) fn lower_array_ptr_seek(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + require_operand_count(inst, "array_ptr_seek", 3)?; + let container = expect_operand(inst, 0)?; + let cursor = expect_operand(inst, 1)?; + let mode = expect_operand(inst, 2)?; + let arg0 = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg1 = abi::int_arg_reg_name(ctx.emitter.target, 1); + let arg2 = abi::int_arg_reg_name(ctx.emitter.target, 2); + ctx.load_value_to_reg(container, arg0)?; + ctx.load_value_to_reg(cursor, arg1)?; + ctx.load_value_to_reg(mode, arg2)?; + abi::emit_call_label(ctx.emitter, "__rt_array_ptr_seek"); + store_if_result(ctx, inst) +} + +/// Lowers `ArrayPtrKey`: `(container, cursor) -> boxed Mixed key`. +/// +/// An out-of-range cursor boxes canonical null inside the helper, which is exactly what +/// PHP's `key()` returns once the internal pointer has run off either end. +pub(super) fn lower_array_ptr_key(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + lower_array_ptr_read(ctx, inst, "array_ptr_key", "__rt_array_ptr_key") +} + +/// Lowers `ArrayPtrValue`: `(container, cursor) -> boxed Mixed value`. +/// +/// An out-of-range cursor boxes `false` inside the helper, matching PHP's return value +/// for `current()` and for a `next`/`prev`/`reset`/`end` that ran out of elements. +pub(super) fn lower_array_ptr_value( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + lower_array_ptr_read(ctx, inst, "array_ptr_value", "__rt_array_ptr_value") +} + +/// Marshals `(container, cursor)` and calls one of the two boxing read helpers. +fn lower_array_ptr_read( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, + symbol: &str, +) -> Result<()> { + require_operand_count(inst, name, 2)?; + let container = expect_operand(inst, 0)?; + let cursor = expect_operand(inst, 1)?; + let arg0 = abi::int_arg_reg_name(ctx.emitter.target, 0); + let arg1 = abi::int_arg_reg_name(ctx.emitter.target, 1); + ctx.load_value_to_reg(container, arg0)?; + ctx.load_value_to_reg(cursor, arg1)?; + abi::emit_call_label(ctx.emitter, symbol); + store_if_result(ctx, inst) +} + +/// Rejects an internal-pointer runtime call that does not carry its cursor operands. +/// +/// These targets are only ever emitted by the internal-pointer argument lowering, which +/// always appends the cursor; anything else means the call escaped through the generic +/// registry path with the plain PHP arity and must not be lowered. +fn require_operand_count(inst: &Instruction, name: &str, expected: usize) -> Result<()> { + if inst.operands.len() != expected { + return Err(CodegenIrError::unsupported(format!( + "{} expects {} typed operands but received {}", + name, + expected, + inst.operands.len(), + ))); + } + Ok(()) +} diff --git a/src/codegen/lower_inst/builtins/arrays/key_exists.rs b/src/codegen/lower_inst/builtins/arrays/key_exists.rs index 6078c65b07..8eeab0a476 100644 --- a/src/codegen/lower_inst/builtins/arrays/key_exists.rs +++ b/src/codegen/lower_inst/builtins/arrays/key_exists.rs @@ -248,7 +248,7 @@ fn materialize_hash_key_aarch64(ctx: &mut FunctionContext<'_>, key: ValueId) -> } PhpType::Float => { ctx.load_value_to_reg(key, "d0")?; - ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "x1"); abi::emit_load_int_immediate(ctx.emitter, "x2", -1); Ok(()) } @@ -283,7 +283,7 @@ fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: ValueId) -> R } PhpType::Float => { ctx.load_value_to_reg(key, "xmm0")?; - ctx.emitter.instruction("cvttsd2si rsi, xmm0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "rsi"); abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); Ok(()) } diff --git a/src/codegen/lower_inst/builtins/arrays/misc_dispatch.rs b/src/codegen/lower_inst/builtins/arrays/misc_dispatch.rs index 56cf3d847f..11d7194312 100644 --- a/src/codegen/lower_inst/builtins/arrays/misc_dispatch.rs +++ b/src/codegen/lower_inst/builtins/arrays/misc_dispatch.rs @@ -8,6 +8,9 @@ //! - Preserves callback ABI, target parity, array storage, and ownership contracts. use super::*; +use super::sort_dispatch::KeySortOrder; +use crate::codegen::lower_inst::receiver_place::ReceiverPlace; +use super::range_size; /// Lowers `array_values()` through the dedicated values-array builtin emitter. pub(crate) fn lower_array_values(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { @@ -33,44 +36,114 @@ pub(crate) fn lower_array_rand(ctx: &mut FunctionContext<'_>, inst: &Instruction } /// Lowers `range()` for integer endpoints through the shared runtime constructor. +/// +/// PHP's optional `$step` becomes the helper's third argument. Its sign never chooses the +/// direction (`start` vs `end` does), so the three `ValueError`s php-src raises for a bad step +/// are emitted here, before the helper ever sees the arguments. pub(crate) fn lower_range(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::super::ensure_arg_count(inst, "range", 2)?; + ensure_arg_count_between(inst, "range", 2, 3)?; let start = expect_operand(inst, 0)?; let end = expect_operand(inst, 1)?; + let step = inst.operands.get(2).copied(); require_range_endpoint(ctx.value_php_type(start)?, "start")?; require_range_endpoint(ctx.value_php_type(end)?, "end")?; + if let Some(step) = step { + require_range_endpoint(ctx.value_php_type(step)?, "step")?; + } require_range_result_type(&inst.result_php_type.codegen_repr())?; - // Resolve each endpoint to a plain integer, unboxing a Mixed cell read from a heterogeneous - // array. The end resolution may call __rt_mixed_cast_int, which clobbers caller-saved registers, - // so the resolved start is spilled across it instead of being staged in an argument register. + // Resolve each argument to a plain integer, unboxing a Mixed cell read from a heterogeneous + // array. Each resolution may call __rt_mixed_cast_int, which clobbers caller-saved registers, + // so already-resolved values are spilled across it instead of being staged in argument registers. resolve_int_operand_to_result(ctx, start, "range start")?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); resolve_int_operand_to_result(ctx, end, "range end")?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match step { + Some(step) => { + resolve_int_operand_to_result(ctx, step, "range step")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x2, x0"); // move the resolved range step into the third runtime argument + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdx, rax"); // move the resolved range step into the third runtime argument + } + } + } + None => { + let step_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x2", + Arch::X86_64 => "rdx", + }; + abi::emit_load_int_immediate(ctx.emitter, step_reg, 1); + } + } match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x1, x0"); // move the resolved range end into the second runtime argument + abi::emit_pop_reg(ctx.emitter, "x1"); // restore the resolved range end into the second runtime argument abi::emit_pop_reg(ctx.emitter, "x0"); // restore the resolved range start into the first runtime argument } Arch::X86_64 => { - ctx.emitter.instruction("mov rsi, rax"); // move the resolved range end into the second runtime argument + abi::emit_pop_reg(ctx.emitter, "rsi"); // restore the resolved range end into the second runtime argument abi::emit_pop_reg(ctx.emitter, "rdi"); // restore the resolved range start into the first runtime argument } } + emit_range_guards(ctx, step.is_some()); abi::emit_call_label(ctx.emitter, "__rt_range"); store_if_result(ctx, inst) } +/// Raises every `range()` `ValueError` reference PHP checks before the runtime helper runs. +/// +/// The guards read `start`/`end`/`step` while they still sit in their ABI argument registers, so +/// one sequence covers every supported target. Reference PHP rejects, in this order: a zero step, +/// a negative step when `$start < $end` (a decreasing range accepts either sign), a step whose +/// magnitude exceeds the spanned interval — except when `$start === $end`, which always yields the +/// single-element `[$start]` — and finally a requested element count past the maximum array size. +/// The magnitude comparison is UNSIGNED so `PHP_INT_MIN`, whose negation is itself, still reads as +/// wider than any span instead of wrapping back to a negative "magnitude". +/// +/// `has_explicit_step` skips the three `$step` guards for a two-argument `range()`: the implicit +/// step is the literal `1` this lowering just materialized, which none of them can reject. The +/// size guard runs either way, because `range(1, 3000000000)` is oversized without any `$step`. +fn emit_range_guards(ctx: &mut FunctionContext<'_>, has_explicit_step: bool) { + let (start_reg, end_reg, step_reg) = match ctx.emitter.target.arch { + Arch::AArch64 => ("x0", "x1", "x2"), + Arch::X86_64 => ("rdi", "rsi", "rdx"), + }; + if has_explicit_step { + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::NotEqualToImmediate(step_reg, 0), + RANGE_ZERO_STEP_MESSAGE, + ); + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::NonNegativeUnlessSignedBelow( + step_reg, start_reg, end_reg, + ), + RANGE_NEGATIVE_STEP_MESSAGE, + ); + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::MagnitudeWithinSpan( + step_reg, start_reg, end_reg, + ), + RANGE_STEP_TOO_WIDE_MESSAGE, + ); + } + range_size::emit_range_size_guard(ctx); +} + /// Lowers `array_pop()` for indexed arrays by mutating length and boxing `T|null` as Mixed. pub(crate) fn lower_array_pop(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::super::ensure_arg_count(inst, "array_pop", 1)?; let array = expect_operand(inst, 0)?; let elem_ty = array_pop_element_type(ctx.value_php_type(array)?)?; require_array_pop_result_type(&inst.result_php_type.codegen_repr())?; - let source_local = source_load_local_slot(ctx, array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_array_pop_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back_value(ctx, array)?; match ctx.emitter.target.arch { Arch::AArch64 => lower_array_pop_aarch64(ctx, array, &elem_ty)?, Arch::X86_64 => lower_array_pop_x86_64(ctx, array, &elem_ty)?, @@ -99,23 +172,37 @@ pub(crate) fn lower_rsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> } /// Lowers `asort()` for indexed integer arrays through the value-sort runtime wrapper. +/// Lowers `asort()`, routing hash receivers to the insertion-order value sorter. +/// +/// A hash-backed associative array keeps its key/value association while its iteration +/// order changes, which `__rt_hash_asort` implements by relinking the table's chain. +/// Indexed arrays have no separate key storage, so they keep using the slot permuter. pub(crate) fn lower_asort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::super::ensure_arg_count(inst, "asort", 1)?; + if sort_receiver_is_hash(ctx, inst)? { + return lower_hash_link_sort(ctx, inst, "__rt_hash_asort"); + } lower_indexed_array_sort(ctx, inst, "asort", "__rt_asort", None) } /// Lowers `arsort()` for indexed integer arrays through the descending value-sort wrapper. +/// Lowers `arsort()`, routing hash receivers to the descending insertion-order value sorter. pub(crate) fn lower_arsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::super::ensure_arg_count(inst, "arsort", 1)?; + if sort_receiver_is_hash(ctx, inst)? { + return lower_hash_link_sort(ctx, inst, "__rt_hash_arsort"); + } lower_indexed_array_sort(ctx, inst, "arsort", "__rt_arsort", None) } /// Lowers `ksort()` through the key-sort helper surface. pub(crate) fn lower_ksort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_array_key_sort(ctx, inst, "ksort", "__rt_ksort") + lower_array_key_sort(ctx, inst, "ksort", KeySortOrder::Ascending) } /// Lowers `krsort()` through the reverse key-sort helper surface. pub(crate) fn lower_krsort(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_array_key_sort(ctx, inst, "krsort", "__rt_krsort") + lower_array_key_sort(ctx, inst, "krsort", KeySortOrder::Descending) } /// Lowers `natsort()` for indexed integer arrays through the natural-sort runtime wrapper. diff --git a/src/codegen/lower_inst/builtins/arrays/pop_search.rs b/src/codegen/lower_inst/builtins/arrays/pop_search.rs index c3285ff076..80152f1dcd 100644 --- a/src/codegen/lower_inst/builtins/arrays/pop_search.rs +++ b/src/codegen/lower_inst/builtins/arrays/pop_search.rs @@ -220,27 +220,6 @@ pub(super) fn emit_array_pop_null(ctx: &mut FunctionContext<'_>) { crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void); } -/// Returns the local slot loaded by an `array_pop()` argument when it came from `load_local`. -pub(super) fn source_load_local_slot( - ctx: &FunctionContext<'_>, - value: ValueId, -) -> Result> { - let Some(value_ref) = ctx.function.value(value) else { - return Err(CodegenIrError::missing_entry("value", value.as_raw())); - }; - let ValueDef::Instruction { inst, .. } = value_ref.def else { - return Ok(None); - }; - let Some(inst_ref) = ctx.function.instruction(inst) else { - return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); - }; - if inst_ref.op == Op::LoadLocal { - if let Some(Immediate::LocalSlot(slot)) = inst_ref.immediate { - return Ok(Some(slot)); - } - } - Ok(None) -} /// Describes which indexed-array `array_search()` lowering path applies. pub(super) enum ArraySearchCase { diff --git a/src/codegen/lower_inst/builtins/arrays/range_size.rs b/src/codegen/lower_inst/builtins/arrays/range_size.rs new file mode 100644 index 0000000000..ad8a318516 --- /dev/null +++ b/src/codegen/lower_inst/builtins/arrays/range_size.rs @@ -0,0 +1,200 @@ +//! Purpose: +//! Raises reference PHP's `"The supplied range exceeds the maximum array size"` `ValueError` +//! for a `range()` whose element count is past PHP's maximum, before `__rt_range` is asked for +//! the allocation. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::arrays::emit_range_guards()`. +//! +//! Key details: +//! - php-src computes `(zend_ulong) high - low) / step` on the NORMALIZED endpoints (`low` is the +//! smaller argument, `step` its magnitude) and refuses the range as soon as that quotient +//! reaches `HT_MAX_SIZE - 1`, i.e. as soon as the array would need more than `2^30 - 1` +//! elements. The subtraction and the division are both unsigned, so `range(PHP_INT_MIN, +//! PHP_INT_MAX)` reports a `2^64 - 1` span instead of wrapping to a small signed one. +//! - The message interpolates the normalized `low`, `high` and `|step|`, so it is built at +//! runtime through `__rt_itoa`/`__rt_concat` and persisted before the throwable takes it over. +//! - The guard reads `$start`/`$end`/`$step` while they still sit in their ABI argument +//! registers, so one sequence covers every supported target; the x86_64 path stages `$step` +//! in a scratch register because the unsigned `div` overwrites `rdx`, which `__rt_range` +//! still needs when the guard passes. + +use crate::codegen::abi; +use crate::codegen::context::FunctionContext; +use crate::codegen::platform::Arch; + +/// `HT_MAX_SIZE - 1`: the element-count-minus-one php-src refuses to build a `range()` for. +/// +/// php-src rejects when `(high - low) / |step| >= HT_MAX_SIZE - 1`, so the largest range it +/// accepts holds `1073741823` elements: `range(1, 1073741823)` is built (and then fails on +/// memory), `range(0, 1073741823)` is a `ValueError`. +const RANGE_MAX_SPAN_STEPS: i64 = 1_073_741_823; + +/// The fixed head of php-src's oversized-range `ValueError`, up to the normalized low endpoint. +const RANGE_SIZE_MESSAGE_PREFIX: &str = + "The supplied range exceeds the maximum array size: start="; + +/// The message fragment php-src writes between the normalized low and high endpoints. +const RANGE_SIZE_MESSAGE_END_SEPARATOR: &str = " end="; + +/// The message fragment php-src writes between the normalized high endpoint and the step. +const RANGE_SIZE_MESSAGE_STEP_SEPARATOR: &str = " step="; + +/// Raises PHP's oversized-range `ValueError` unless the requested element count fits an array. +/// +/// Expects `$start`, `$end` and `$step` in `__rt_range`'s first three ABI argument registers and +/// leaves them untouched on the accepted path. A rejected range never returns: the failure path +/// builds php-src's exact message from the normalized endpoints and hands it to the unwinder. +pub(super) fn emit_range_size_guard(ctx: &mut FunctionContext<'_>) { + let ok_label = ctx.next_label("range_size_ok"); + let normalized = match ctx.emitter.target.arch { + Arch::AArch64 => emit_span_check_aarch64(ctx, &ok_label), + Arch::X86_64 => emit_span_check_x86_64(ctx, &ok_label), + }; + emit_range_size_value_error(ctx, normalized); + ctx.emitter.label(&ok_label); +} + +/// The registers holding the normalized interval on the guard's failure path. +/// +/// `low`/`high` are the ordered endpoints and `step` their stride magnitude — exactly the three +/// values php-src interpolates into the message, in the order it prints them. +struct NormalizedRange { + /// Register holding the smaller of `$start` and `$end`. + low: &'static str, + /// Register holding the larger of `$start` and `$end`. + high: &'static str, + /// Register holding `abs($step)`. + step: &'static str, +} + +/// Emits the AArch64 span check and branches to `ok_label` for a range that fits an array. +/// +/// Normalizes the endpoints with `csel`, the step with `cneg`, then divides the unsigned span by +/// the stride. `udiv` by zero answers zero on AArch64, so even a step the caller failed to reject +/// cannot fault here. +fn emit_span_check_aarch64(ctx: &mut FunctionContext<'_>, ok_label: &str) -> NormalizedRange { + ctx.emitter.instruction("cmp x0, x1"); // is the requested interval increasing? + ctx.emitter.instruction("csel x9, x0, x1, le"); // x9 = low, the smaller of start and end + ctx.emitter.instruction("csel x10, x1, x0, le"); // x10 = high, the larger of start and end + ctx.emitter.instruction("cmp x2, #0"); // is the requested step negative? + ctx.emitter.instruction("cneg x11, x2, mi"); // x11 = |step|, the stride PHP counts with + ctx.emitter.instruction("sub x12, x10, x9"); // x12 = high - low, the spanned interval as an unsigned width + ctx.emitter.instruction("udiv x12, x12, x11"); // x12 = span / |step|, PHP's element count minus one + abi::emit_load_int_immediate(ctx.emitter, "x13", RANGE_MAX_SPAN_STEPS); + ctx.emitter.instruction("cmp x12, x13"); // compare the requested element count against PHP's maximum array size + ctx.emitter.instruction(&format!("b.lo {}", ok_label)); // a range below the maximum array size is built normally + NormalizedRange { + low: "x9", + high: "x10", + step: "x11", + } +} + +/// Emits the x86_64 span check and branches to `ok_label` for a range that fits an array. +/// +/// `div` reads its dividend from `rdx:rax` and writes the remainder back to `rdx`, which still +/// carries `$step` for the call that follows, so the step is staged in `r9` and restored right +/// after the divide. +fn emit_span_check_x86_64(ctx: &mut FunctionContext<'_>, ok_label: &str) -> NormalizedRange { + ctx.emitter.instruction("mov r10, rdi"); // stage start as the interval low endpoint + ctx.emitter.instruction("mov r11, rsi"); // stage end as the interval high endpoint + ctx.emitter.instruction("cmp rdi, rsi"); // is the requested interval decreasing? + ctx.emitter.instruction("cmovg r10, rsi"); // r10 = low, the smaller of start and end + ctx.emitter.instruction("cmovg r11, rdi"); // r11 = high, the larger of start and end + ctx.emitter.instruction("mov r9, rdx"); // preserve the step argument across the unsigned divide + ctx.emitter.instruction("mov rcx, rdx"); // stage the step before normalizing its magnitude + ctx.emitter.instruction("neg rcx"); // negate the step so a negative one yields its magnitude + ctx.emitter.instruction("test rdx, rdx"); // is the requested step negative? + ctx.emitter.instruction("cmovns rcx, rdx"); // rcx = |step|, the stride PHP counts with + ctx.emitter.instruction("mov rax, r11"); // stage the interval high endpoint before subtracting the low one + ctx.emitter.instruction("sub rax, r10"); // rax = high - low, the spanned interval as an unsigned width + ctx.emitter.instruction("xor edx, edx"); // clear the dividend high word for the unsigned divide + ctx.emitter.instruction("div rcx"); // rax = span / |step|, PHP's element count minus one + ctx.emitter.instruction("mov rdx, r9"); // restore the step argument for the range helper + abi::emit_load_int_immediate(ctx.emitter, "r8", RANGE_MAX_SPAN_STEPS); + ctx.emitter.instruction("cmp rax, r8"); // compare the requested element count against PHP's maximum array size + ctx.emitter.instruction(&format!("jb {}", ok_label)); // a range below the maximum array size is built normally + NormalizedRange { + low: "r10", + high: "r11", + step: "rcx", + } +} + +/// Builds php-src's oversized-range message from the normalized interval and throws it. +/// +/// The three integers are parked in one 32-byte temporary first, because `__rt_itoa` and +/// `__rt_concat` both clobber every caller-saved register. The partially built message is parked +/// the same way across each following `__rt_itoa`, so only one concat result is ever live in +/// registers at a time. Control never returns from here. +fn emit_range_size_value_error(ctx: &mut FunctionContext<'_>, normalized: NormalizedRange) { + // Temporary layout after both pushes: [0]=|step|, [16]=low, [24]=high. + abi::emit_push_reg_pair(ctx.emitter, normalized.low, normalized.high); + abi::emit_push_reg(ctx.emitter, normalized.step); + emit_itoa_from_temporary_slot(ctx, 16); + emit_concat_static_prefix(ctx, RANGE_SIZE_MESSAGE_PREFIX); + emit_concat_static_suffix(ctx, RANGE_SIZE_MESSAGE_END_SEPARATOR); + emit_concat_temporary_slot_integer(ctx, 40); + emit_concat_static_suffix(ctx, RANGE_SIZE_MESSAGE_STEP_SEPARATOR); + emit_concat_temporary_slot_integer(ctx, 16); + abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + crate::codegen::lower_inst::exceptions::emit_value_error_from_string_result(ctx); +} + +/// Converts the integer parked at `offset` in the temporary stack to its decimal digits. +/// +/// Leaves the digits in the target's string-result registers, which is where `__rt_concat` +/// expects its left operand. +fn emit_itoa_from_temporary_slot(ctx: &mut FunctionContext<'_>, offset: usize) { + let integer_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, integer_reg, offset); + abi::emit_call_label(ctx.emitter, "__rt_itoa"); +} + +/// Prepends a static fragment to the string currently held in the string-result registers. +fn emit_concat_static_prefix(ctx: &mut FunctionContext<'_>, prefix: &str) { + let (text_ptr, text_len) = abi::string_result_regs(ctx.emitter); + let (right_ptr, right_len) = concat_right_operand_regs(ctx); + let (prefix_label, prefix_len) = ctx.data.add_string(prefix.as_bytes()); + ctx.emitter.instruction(&format!("mov {}, {}", right_ptr, text_ptr)); // move the built text into the concat right operand + ctx.emitter.instruction(&format!("mov {}, {}", right_len, text_len)); // move its length into the concat right operand + abi::emit_symbol_address(ctx.emitter, text_ptr, &prefix_label); + abi::emit_load_int_immediate(ctx.emitter, text_len, prefix_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_concat"); +} + +/// Appends a static fragment to the string currently held in the string-result registers. +fn emit_concat_static_suffix(ctx: &mut FunctionContext<'_>, suffix: &str) { + let (right_ptr, right_len) = concat_right_operand_regs(ctx); + let (suffix_label, suffix_len) = ctx.data.add_string(suffix.as_bytes()); + abi::emit_symbol_address(ctx.emitter, right_ptr, &suffix_label); + abi::emit_load_int_immediate(ctx.emitter, right_len, suffix_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_concat"); +} + +/// Appends the decimal digits of the integer parked at `offset` to the built message. +/// +/// The message itself is parked in a fresh 16-byte temporary across `__rt_itoa`, so `offset` must +/// already account for that push — the caller passes the deeper offset. +fn emit_concat_temporary_slot_integer(ctx: &mut FunctionContext<'_>, offset: usize) { + let (text_ptr, text_len) = abi::string_result_regs(ctx.emitter); + let (right_ptr, right_len) = concat_right_operand_regs(ctx); + abi::emit_push_reg_pair(ctx.emitter, text_ptr, text_len); + emit_itoa_from_temporary_slot(ctx, offset); + ctx.emitter.instruction(&format!("mov {}, {}", right_ptr, text_ptr)); // move the fresh digits into the concat right operand + ctx.emitter.instruction(&format!("mov {}, {}", right_len, text_len)); // move their length into the concat right operand + abi::emit_load_temporary_stack_slot(ctx.emitter, text_ptr, 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, text_len, 8); + abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_call_label(ctx.emitter, "__rt_concat"); +} + +/// Returns the registers `__rt_concat` reads its right operand pointer/length from. +fn concat_right_operand_regs(ctx: &FunctionContext<'_>) -> (&'static str, &'static str) { + match ctx.emitter.target.arch { + Arch::AArch64 => ("x3", "x4"), + Arch::X86_64 => ("rdi", "rsi"), + } +} diff --git a/src/codegen/lower_inst/builtins/arrays/reduce_sets.rs b/src/codegen/lower_inst/builtins/arrays/reduce_sets.rs index a50cf9f96b..b73a26b962 100644 --- a/src/codegen/lower_inst/builtins/arrays/reduce_sets.rs +++ b/src/codegen/lower_inst/builtins/arrays/reduce_sets.rs @@ -8,6 +8,7 @@ //! - Preserves callback ABI, target parity, array storage, and ownership contracts. use super::*; +use crate::codegen::lower_inst::receiver_place::ReceiverPlace; /// Lowers `array_reduce()` through the callback-driven runtime helper. pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { @@ -15,10 +16,10 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi let array = expect_operand(inst, 0)?; let callback = expect_operand(inst, 1)?; let initial = expect_operand(inst, 2)?; - let elem_ty = - eight_byte_callback_array_element_type(ctx.value_php_type(array)?, "array_reduce")?; + let elem_ty = array_reduce_callback_array_element_type(ctx.value_php_type(array)?)?; let initial_ty = eight_byte_callback_value_type(ctx.value_php_type(initial)?, "array_reduce initial")?; + let reduce_helper = array_reduce_runtime_label(&elem_ty); match ctx.value_php_type(callback)?.codegen_repr() { PhpType::Callable => { lower_descriptor_callback_runtime( @@ -35,7 +36,7 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_array_reduce"); + abi::emit_call_label(ctx.emitter, reduce_helper); Ok(()) }, )?; @@ -50,7 +51,7 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi Some(&PhpType::Array(Box::new(elem_ty.clone()))), vec![initial_ty.clone(), elem_ty.clone()], PhpType::Int, - super::super::super::instruction_strict_php_profile(inst), + super::super::instruction_strict_php_profile(inst), "array_reduce", |ctx, wrapper_label, env_bytes| { let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); @@ -61,7 +62,7 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_array_reduce"); + abi::emit_call_label(ctx.emitter, reduce_helper); Ok(()) }, )?; @@ -86,7 +87,7 @@ pub(crate) fn lower_array_reduce(ctx: &mut FunctionContext<'_>, inst: &Instructi ctx.load_value_to_reg(array, array_arg_reg)?; ctx.load_value_to_reg(initial, initial_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_array_reduce"); + abi::emit_call_label(ctx.emitter, reduce_helper); if env_bytes != 0 { abi::emit_release_temporary_stack(ctx.emitter, env_bytes); } @@ -228,9 +229,25 @@ pub(crate) fn lower_array_intersect_key( } /// Lowers `array_slice()` for indexed arrays with pointer-sized payload slots. +/// +/// PHP's `bool $preserve_keys = false` keeps the source integer keys of the selected window +/// instead of renumbering it from zero. A dense indexed array cannot hold a window that does not +/// start at key 0, so the key-preserving form lowers to `__rt_array_slice_to_hash`, which builds an +/// owned hash. The checker guarantees the flag is a literal (it decides the result's static +/// shape), so a non-literal operand can only mean the checker and the backend disagree. +/// Lowers `array_slice()` for indexed arrays with pointer-sized payload slots. +/// +/// PHP's `bool $preserve_keys = false` keeps the source integer keys of the selected window +/// instead of renumbering it from zero. A dense indexed array cannot hold a window that does not +/// start at key 0, so the key-preserving form lowers to `__rt_array_slice_to_hash`, which builds an +/// owned hash. The checker guarantees the flag is a literal (it decides the result's static +/// shape), so a non-literal operand can only mean the checker and the backend disagree. pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - ensure_arg_count_between(inst, "array_slice", 2, 3)?; + ensure_arg_count_between(inst, "array_slice", 2, 4)?; let array = expect_operand(inst, 0)?; + if slice_like_preserve_keys(ctx, inst, "array_slice")? { + return lower_array_slice_preserve_keys(ctx, inst, array); + } if matches!( ctx.value_php_type(array)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_) @@ -238,11 +255,7 @@ pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instructio return lower_mixed_array_slice(ctx, inst); } let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { - Some(expect_operand(inst, 2)?) - } else { - None - }; + let length = slice_like_length_operand(inst)?; let source_elem_ty = array_slice_source_element_type(ctx.value_php_type(array)?)?; let result_elem_ty = result_array_element_type("array_slice", &inst.result_php_type.codegen_repr())?; @@ -256,11 +269,7 @@ pub(crate) fn lower_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instructio pub(super) fn lower_mixed_array_slice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let array = expect_operand(inst, 0)?; let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { - Some(expect_operand(inst, 2)?) - } else { - None - }; + let length = slice_like_length_operand(inst)?; let result_elem_ty = result_array_element_type("array_slice", &inst.result_php_type.codegen_repr())?; require_array_slice_result_type(&PhpType::Mixed, &result_elem_ty)?; @@ -273,8 +282,13 @@ pub(super) fn lower_mixed_array_slice(ctx: &mut FunctionContext<'_>, inst: &Inst } /// Lowers `array_splice()` by mutating an indexed source array and returning removed elements. +/// +/// PHP's optional `$replacement` is written into the gap the removal opened, which can make the +/// source array longer than it was. `__rt_array_splice_insert*` grows the payload for that, and a +/// growth relocates the array, so the by-reference receiver is written back a second time after +/// the insertion rather than only after the copy-on-write split. pub(crate) fn lower_array_splice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - ensure_arg_count_between(inst, "array_splice", 2, 3)?; + ensure_arg_count_between(inst, "array_splice", 2, 4)?; let array = expect_operand(inst, 0)?; if matches!( ctx.value_php_type(array)?.codegen_repr(), @@ -283,18 +297,16 @@ pub(crate) fn lower_array_splice(ctx: &mut FunctionContext<'_>, inst: &Instructi return lower_mixed_array_splice(ctx, inst); } let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { - Some(expect_operand(inst, 2)?) - } else { - None - }; + let length = inst.operands.get(2).copied(); let elem_ty = array_pop_element_type(ctx.value_php_type(array)?)?; - let source_local = source_load_local_slot(ctx, array)?; + let replacement = + SpliceReplacement::resolve(ctx, inst.operands.get(3).copied(), &elem_ty)?; + let receiver_ty = ctx.value_php_type(array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_array_pop_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back(ctx, array, &receiver_ty)?; lower_array_splice_call(ctx, array, offset, length, &elem_ty)?; + emit_splice_replacement_insert(ctx, array, receiver, &receiver_ty, &replacement, &elem_ty)?; normalize_array_splice_result(ctx, &elem_ty, &inst.result_php_type.codegen_repr())?; store_if_result(ctx, inst) } @@ -303,16 +315,17 @@ pub(crate) fn lower_array_splice(ctx: &mut FunctionContext<'_>, inst: &Instructi pub(super) fn lower_mixed_array_splice(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let array = expect_operand(inst, 0)?; let offset = expect_operand(inst, 1)?; - let length = if inst.operands.len() == 3 { - Some(expect_operand(inst, 2)?) - } else { - None - }; + let length = inst.operands.get(2).copied(); + let replacement = + SpliceReplacement::resolve(ctx, inst.operands.get(3).copied(), &PhpType::Mixed)?; match ctx.emitter.target.arch { - Arch::AArch64 => lower_mixed_array_splice_aarch64(ctx, array, offset, length)?, - Arch::X86_64 => lower_mixed_array_splice_x86_64(ctx, array, offset, length)?, + Arch::AArch64 => { + lower_mixed_array_splice_aarch64(ctx, array, offset, length, &replacement)? + } + Arch::X86_64 => { + lower_mixed_array_splice_x86_64(ctx, array, offset, length, &replacement)? + } } normalize_array_splice_result(ctx, &PhpType::Mixed, &inst.result_php_type.codegen_repr())?; store_if_result(ctx, inst) } - diff --git a/src/codegen/lower_inst/builtins/arrays/shift.rs b/src/codegen/lower_inst/builtins/arrays/shift.rs index dabac42b1d..2acb3e15ab 100644 --- a/src/codegen/lower_inst/builtins/arrays/shift.rs +++ b/src/codegen/lower_inst/builtins/arrays/shift.rs @@ -6,7 +6,8 @@ //! - `crate::codegen::lower_inst::builtins::arrays::lower_array_shift()`. //! //! Key details: -//! - Mutates the caller-visible array after copy-on-write splitting. +//! - Mutates the caller-visible array after copy-on-write splitting, publishing the split +//! pointer through `ReceiverPlace` so a by-reference parameter observes it as well. //! - Returns PHP `mixed`, including boxed null for empty arrays. //! - Supports pointer-sized, float, string, Mixed, and refcounted indexed payloads. @@ -17,6 +18,7 @@ use crate::codegen::{CodegenIrError, Result}; use crate::ir::{Instruction, ValueId}; use crate::types::PhpType; +use super::super::super::receiver_place::ReceiverPlace; use super::super::super::{expect_operand, store_if_result}; /// Lowers `array_shift()` for indexed arrays by compacting slots and boxing `T|null` as Mixed. @@ -25,11 +27,9 @@ pub(super) fn lower_array_shift(ctx: &mut FunctionContext<'_>, inst: &Instructio let array = expect_operand(inst, 0)?; let elem_ty = array_shift_element_type(ctx.value_php_type(array)?)?; require_array_shift_result_type(&inst.result_php_type.codegen_repr())?; - let source_local = super::source_load_local_slot(ctx, array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_array_shift_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back_value(ctx, array)?; match ctx.emitter.target.arch { Arch::AArch64 => lower_array_shift_aarch64(ctx, array, &elem_ty)?, Arch::X86_64 => lower_array_shift_x86_64(ctx, array, &elem_ty)?, diff --git a/src/codegen/lower_inst/builtins/arrays/slice_splice.rs b/src/codegen/lower_inst/builtins/arrays/slice_splice.rs index 18fa698fc7..e5ef21240c 100644 --- a/src/codegen/lower_inst/builtins/arrays/slice_splice.rs +++ b/src/codegen/lower_inst/builtins/arrays/slice_splice.rs @@ -37,12 +37,29 @@ pub(super) fn lower_array_splice_call( /// Materializes the shared `(array, offset, length)` argument triple for `array_slice` and /// `array_splice` into the runtime argument registers. +/// Materializes the shared `(array, offset, length, length_present)` argument tuple for +/// `array_slice` and `array_splice` into the runtime argument registers. /// /// The offset and length are resolved to plain integers first — unboxing a `Mixed` cell read from a /// heterogeneous array via `__rt_mixed_cast_int` — and spilled to the stack, because that unbox call /// clobbers caller-saved registers. The array pointer (a plain stack load that clobbers nothing) is /// then placed, and the staged integers are restored into the offset/length argument registers, so /// the runtime helper sees the array pointer plus two genuine integers rather than a boxed pointer. +/// The offset, the length and the length-present flag are resolved to plain integers first — +/// unboxing a `Mixed` cell read from a heterogeneous array via `__rt_mixed_cast_int` — and spilled to +/// the stack, because those unbox calls clobber caller-saved registers. The array pointer (a plain +/// stack load that clobbers nothing) is then placed, and the staged integers are restored into the +/// offset/length/flag argument registers, so the runtime helper sees the array pointer plus three +/// genuine integers rather than a boxed pointer. +/// Materializes the shared `(array, offset, length, length_present)` argument tuple for +/// `array_slice` and `array_splice` into the runtime argument registers. +/// +/// The offset, the length and the length-present flag are resolved to plain integers first — +/// unboxing a `Mixed` cell read from a heterogeneous array via `__rt_mixed_cast_int` — and spilled to +/// the stack, because those unbox calls clobber caller-saved registers. The array pointer (a plain +/// stack load that clobbers nothing) is then placed, and the staged integers are restored into the +/// offset/length/flag argument registers, so the runtime helper sees the array pointer plus three +/// genuine integers rather than a boxed pointer. pub(super) fn lower_slice_like_args( ctx: &mut FunctionContext<'_>, array: ValueId, @@ -52,17 +69,21 @@ pub(super) fn lower_slice_like_args( ) -> Result<()> { resolve_int_operand_to_result(ctx, offset, &format!("{} offset", name))?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + resolve_slice_length_present_to_result(ctx, length)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); resolve_slice_length_to_result(ctx, length, name)?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); match ctx.emitter.target.arch { Arch::AArch64 => { ctx.load_value_to_reg(array, "x0")?; abi::emit_pop_reg(ctx.emitter, "x2"); // restore the resolved length into the third runtime argument + abi::emit_pop_reg(ctx.emitter, "x3"); // restore the length-present flag into the fourth runtime argument abi::emit_pop_reg(ctx.emitter, "x1"); // restore the resolved offset into the second runtime argument } Arch::X86_64 => { ctx.load_value_to_reg(array, "rdi")?; abi::emit_pop_reg(ctx.emitter, "rdx"); // restore the resolved length into the third runtime argument + abi::emit_pop_reg(ctx.emitter, "rcx"); // restore the length-present flag into the fourth runtime argument abi::emit_pop_reg(ctx.emitter, "rsi"); // restore the resolved offset into the second runtime argument } } @@ -71,20 +92,17 @@ pub(super) fn lower_slice_like_args( /// Resolves an optional `array_slice`/`array_splice` length into the integer result register. /// -/// An absent or `Void` length becomes the runtime "until the end" sentinel; otherwise the length is -/// resolved through the shared integer resolver, unboxing a `Mixed` value to a plain integer. +/// An absent or `Void` length materializes a zero placeholder that the helper ignores because the +/// companion length-present flag is zero; otherwise the length is resolved through the shared integer +/// resolver, unboxing a `Mixed` value to a plain integer. pub(super) fn resolve_slice_length_to_result( ctx: &mut FunctionContext<'_>, length: Option, name: &str, ) -> Result<()> { - let until_end = match length { - None => true, - Some(length) => matches!(ctx.value_php_type(length)?.codegen_repr(), PhpType::Void), - }; - if until_end { + if slice_length_is_statically_absent(ctx, length)? { let reg = abi::int_result_reg(ctx.emitter); - emit_array_slice_until_end_sentinel(ctx, reg); + abi::emit_load_int_immediate(ctx.emitter, reg, 0); return Ok(()); } resolve_int_operand_to_result( @@ -98,11 +116,12 @@ pub(super) fn resolve_slice_length_to_result( /// refcounted runtime helper's argument registers, restoring a previously-staged array pointer. /// /// On entry the converted (now-owned) indexed-array pointer must be the topmost value on the -/// temporary stack. The offset and length are resolved to plain integers first — `__rt_mixed_cast_int` -/// unboxes a `Mixed` cell read from a heterogeneous array, and an absent/`Void` length becomes the -/// until-the-end sentinel — and spilled to the stack, because each unbox call clobbers caller-saved -/// registers. The three staged values are then popped into the array/offset/length argument registers -/// so the helper sees a pointer plus two genuine integers rather than a boxed pointer. +/// temporary stack. The offset, the length and the length-present flag are resolved to plain integers +/// first — `__rt_mixed_cast_int` unboxes a `Mixed` cell read from a heterogeneous array, and an +/// absent/`Void`/boxed-null length clears the length-present flag — and spilled to the stack, because +/// each unbox call clobbers caller-saved registers. The four staged values are then popped into the +/// array/offset/length/flag argument registers so the helper sees a pointer plus three genuine +/// integers rather than a boxed pointer. pub(super) fn materialize_mixed_slice_args( ctx: &mut FunctionContext<'_>, offset: ValueId, @@ -111,16 +130,20 @@ pub(super) fn materialize_mixed_slice_args( ) -> Result<()> { resolve_int_operand_to_result(ctx, offset, &format!("{} offset", name))?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + resolve_slice_length_present_to_result(ctx, length)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); resolve_slice_length_to_result(ctx, length, name)?; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_pop_reg(ctx.emitter, "x2"); // restore the resolved length into the third runtime argument + abi::emit_pop_reg(ctx.emitter, "x3"); // restore the length-present flag into the fourth runtime argument abi::emit_pop_reg(ctx.emitter, "x1"); // restore the resolved offset into the second runtime argument abi::emit_pop_reg(ctx.emitter, "x0"); // restore the converted array pointer into the first runtime argument } Arch::X86_64 => { abi::emit_pop_reg(ctx.emitter, "rdx"); // restore the resolved length into the third runtime argument + abi::emit_pop_reg(ctx.emitter, "rcx"); // restore the length-present flag into the fourth runtime argument abi::emit_pop_reg(ctx.emitter, "rsi"); // restore the resolved offset into the second runtime argument abi::emit_pop_reg(ctx.emitter, "rdi"); // restore the converted array pointer into the first runtime argument } @@ -200,6 +223,7 @@ pub(super) fn lower_mixed_array_splice_aarch64( array: ValueId, offset: ValueId, length: Option, + replacement: &SpliceReplacement, ) -> Result<()> { let drop_label = ctx.next_label("mixed_array_splice_empty"); let done_label = ctx.next_label("mixed_array_splice_done"); @@ -219,6 +243,7 @@ pub(super) fn lower_mixed_array_splice_aarch64( abi::emit_push_reg(ctx.emitter, "x0"); materialize_mixed_slice_args(ctx, offset, length, "array_splice")?; abi::emit_call_label(ctx.emitter, "__rt_array_splice_refcounted"); + emit_mixed_splice_replacement_insert(ctx, array, replacement)?; ctx.emitter.instruction(&format!("b {}", done_label)); // skip the empty-array fallback after splicing the boxed payload ctx.emitter.label(&drop_label); abi::emit_pop_reg(ctx.emitter, "x9"); @@ -233,6 +258,7 @@ pub(super) fn lower_mixed_array_splice_x86_64( array: ValueId, offset: ValueId, length: Option, + replacement: &SpliceReplacement, ) -> Result<()> { let drop_label = ctx.next_label("mixed_array_splice_empty"); let done_label = ctx.next_label("mixed_array_splice_done"); @@ -252,6 +278,7 @@ pub(super) fn lower_mixed_array_splice_x86_64( abi::emit_push_reg(ctx.emitter, "rax"); materialize_mixed_slice_args(ctx, offset, length, "array_splice")?; abi::emit_call_label(ctx.emitter, "__rt_array_splice_refcounted"); + emit_mixed_splice_replacement_insert(ctx, array, replacement)?; ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the empty-array fallback after splicing the boxed payload ctx.emitter.label(&drop_label); abi::emit_pop_reg(ctx.emitter, "r11"); @@ -307,7 +334,7 @@ pub(super) fn lower_array_chunk_call( ctx: &mut FunctionContext<'_>, array: ValueId, length: ValueId, - source_elem_ty: &PhpType, + runtime_label: &str, ) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { @@ -319,7 +346,8 @@ pub(super) fn lower_array_chunk_call( ctx.load_value_to_reg(length, "rsi")?; } } - abi::emit_call_label(ctx.emitter, array_chunk_runtime_helper(source_elem_ty)); + emit_array_chunk_length_guard(ctx); + abi::emit_call_label(ctx.emitter, runtime_label); Ok(()) } @@ -343,20 +371,45 @@ pub(super) fn lower_array_pad_call( ctx.load_value_to_reg(pad_value, "rdx")?; } } + emit_array_pad_length_guard(ctx); abi::emit_call_label(ctx.emitter, array_pad_runtime_helper(source_elem_ty)); Ok(()) } -/// Emits the `-1` runtime sentinel used when slicing to the end of the source array. -pub(super) fn emit_array_slice_until_end_sentinel(ctx: &mut FunctionContext<'_>, reg: &str) { - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, #-1", reg)); // use -1 as the array_slice() runtime sentinel for length until the end - } - Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, -1", reg)); // use -1 as the x86_64 array_slice() runtime sentinel for length until the end - } - } + +/// The largest `array_pad()` `$length` magnitude reference PHP will build an array for. +/// +/// php-src rejects anything past `HT_MAX_SIZE / 2` before it looks at the input array, so +/// the bound is a plain constant: `array_pad($a, 1073741824, …)` is accepted (and then +/// fails on memory), `array_pad($a, 1073741825, …)` is a `ValueError` for every `$a`. +const ARRAY_PAD_MAX_LENGTH: i64 = 1_073_741_824; + +/// php-src's verbatim `ValueError` wording for an oversized `array_pad()` `$length`. +const ARRAY_PAD_LENGTH_TOO_LARGE_MESSAGE: &str = + "array_pad(): Argument #2 ($length) must not exceed the maximum allowed array size"; + +/// Rejects the `array_pad()` `$length` magnitudes reference PHP refuses to build an array for. +/// +/// The pad helpers derive the destination capacity and the destination header length from +/// `abs($length)`, and that absolute value was never bounded: a huge magnitude asked the +/// allocator for a payload the process cannot own, and `PHP_INT_MIN` has no representable +/// magnitude at all, so the negation wrapped straight back to a negative "length". Bounding +/// the signed argument here — before it reaches either helper — keeps both out of reach and +/// raises PHP's catchable `ValueError` in their place. `$length` sits in the second ABI +/// argument register for every pad helper on every supported target. +fn emit_array_pad_length_guard(ctx: &mut FunctionContext<'_>) { + let length_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rsi", + }; + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::SignedMagnitudeAtMost( + length_reg, + ARRAY_PAD_MAX_LENGTH, + ), + ARRAY_PAD_LENGTH_TOO_LARGE_MESSAGE, + ); } /// Returns the helper that matches the chunk source element ownership representation. @@ -388,7 +441,12 @@ pub(super) fn array_slice_runtime_helper(source_elem_ty: &PhpType) -> &'static s /// Returns the helper that matches the spliced element ownership representation. pub(super) fn array_splice_runtime_helper(elem_ty: &PhpType) -> &'static str { - if elem_ty.is_refcounted() { + if elem_ty.codegen_repr() == PhpType::Str { + // Indexed string arrays store 16-byte `{pointer, length}` slots; the shared helpers copy + // and compact 8 bytes at a time, which returned raw pointers as PHP integers and left + // the receiver half-shifted. + "__rt_array_splice_str" + } else if elem_ty.is_refcounted() { "__rt_array_splice_refcounted" } else { "__rt_array_splice" diff --git a/src/codegen/lower_inst/builtins/arrays/sort_dispatch.rs b/src/codegen/lower_inst/builtins/arrays/sort_dispatch.rs index 6e8c11b2fd..3b0b4b6477 100644 --- a/src/codegen/lower_inst/builtins/arrays/sort_dispatch.rs +++ b/src/codegen/lower_inst/builtins/arrays/sort_dispatch.rs @@ -8,6 +8,7 @@ //! - Preserves callback ABI, target parity, array storage, and ownership contracts. use super::*; +use crate::codegen::lower_inst::receiver_place::ReceiverPlace; /// Loads an indexed array argument and calls the selected runtime aggregate helper. pub(super) fn lower_indexed_array_aggregate( @@ -120,11 +121,9 @@ pub(super) fn lower_indexed_array_sort( let array = expect_operand(inst, 0)?; let elem_ty = indexed_sort_element_type(ctx.value_php_type(array)?, name, str_helper.is_some())?; - let source_local = source_load_local_slot(ctx, array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_sort_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back_value(ctx, array)?; match ctx.emitter.target.arch { Arch::AArch64 => { ctx.load_value_to_reg(array, "x0")?; @@ -152,11 +151,9 @@ pub(super) fn lower_indexed_array_shuffle(ctx: &mut FunctionContext<'_>, inst: & super::super::ensure_arg_count(inst, "shuffle", 1)?; let array = expect_operand(inst, 0)?; eight_byte_indexed_array_element_type(ctx.value_php_type(array)?, "shuffle")?; - let source_local = source_load_local_slot(ctx, array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_sort_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back_value(ctx, array)?; match ctx.emitter.target.arch { Arch::AArch64 => { ctx.load_value_to_reg(array, "x0")?; @@ -184,12 +181,11 @@ pub(super) fn lower_user_sort_static_callback( let array = expect_operand(inst, 0)?; let callback = expect_operand(inst, 1)?; let elem_ty = user_sort_element_type(ctx.value_php_type(array)?, name)?; + let sort_helper = user_sort_runtime_label(&elem_ty); let callback_arg_types = [elem_ty.clone(), elem_ty]; - let source_local = source_load_local_slot(ctx, array)?; + let receiver = ReceiverPlace::resolve(ctx, array)?; ensure_unique_sort_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; - } + receiver.store_back_value(ctx, array)?; let callback_ty = ctx.value_php_type(callback)?.codegen_repr(); let callback_owner = format!("{} callback", name); if callback_ty == PhpType::Callable && static_callback_operand_is_recoverable(ctx, callback) { @@ -199,7 +195,13 @@ pub(super) fn lower_user_sort_static_callback( &callback_owner, Some(&callback_arg_types), )?; - return lower_user_sort_with_static_callback_binding(ctx, inst, array, callback_binding); + return lower_user_sort_with_static_callback_binding( + ctx, + inst, + array, + callback_binding, + sort_helper, + ); } match callback_ty { PhpType::Callable => { @@ -215,7 +217,7 @@ pub(super) fn lower_user_sort_static_callback( abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_usort"); + abi::emit_call_label(ctx.emitter, sort_helper); Ok(()) }, )?; @@ -234,7 +236,7 @@ pub(super) fn lower_user_sort_static_callback( Some(&PhpType::Array(Box::new(callback_arg_types[0].clone()))), callback_arg_types.to_vec(), PhpType::Int, - super::super::super::instruction_strict_php_profile(inst), + super::super::instruction_strict_php_profile(inst), name, |ctx, wrapper_label, env_bytes| { let callback_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); @@ -243,7 +245,7 @@ pub(super) fn lower_user_sort_static_callback( abi::emit_symbol_address(ctx.emitter, callback_arg_reg, wrapper_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_usort"); + abi::emit_call_label(ctx.emitter, sort_helper); Ok(()) }, )?; @@ -263,15 +265,20 @@ pub(super) fn lower_user_sort_static_callback( &callback_owner, Some(&callback_arg_types), )?; - lower_user_sort_with_static_callback_binding(ctx, inst, array, callback_binding) + lower_user_sort_with_static_callback_binding(ctx, inst, array, callback_binding, sort_helper) } /// Calls the user-sort runtime with a statically recovered callback binding. +/// +/// `sort_helper` selects the slot permuter matching the receiver's element +/// width: `__rt_usort` for 8-byte payload slots, `__rt_usort_str` for the +/// 16-byte `[ptr][len]` string descriptors. pub(super) fn lower_user_sort_with_static_callback_binding( ctx: &mut FunctionContext<'_>, inst: &Instruction, array: ValueId, callback_binding: StaticSortCallbackBinding, + sort_helper: &str, ) -> Result<()> { let callback_label = sort_callback_label_returning_int(ctx, &callback_binding)?; let env_bytes = reserve_static_callback_env(ctx, callback_binding.env_source)?; @@ -281,7 +288,7 @@ pub(super) fn lower_user_sort_with_static_callback_binding( abi::emit_symbol_address(ctx.emitter, callback_arg_reg, &callback_label); ctx.load_value_to_reg(array, array_arg_reg)?; load_static_callback_env_arg(ctx, env_arg_reg, env_bytes); - abi::emit_call_label(ctx.emitter, "__rt_usort"); + abi::emit_call_label(ctx.emitter, sort_helper); if env_bytes != 0 { abi::emit_release_temporary_stack(ctx.emitter, env_bytes); } @@ -374,30 +381,63 @@ pub(super) fn move_sort_callback_int_result_to_first_arg(ctx: &mut FunctionConte } /// Calls the key-sort helper for array-like values. +/// Direction of a PHP key sort (`ksort` versus `krsort`). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum KeySortOrder { + /// Ascending key order, as produced by `ksort()`. + Ascending, + /// Descending key order, as produced by `krsort()`. + Descending, +} + +/// Lowers `ksort()` / `krsort()` for every receiver shape the backend can represent. +/// +/// Hash-backed associative arrays are reordered by `__rt_hash_ksort` / `__rt_hash_krsort`, +/// which relink the table's insertion-order chain and therefore keep each key attached to +/// its own value. An indexed array stores its keys implicitly as slot positions `0..n-1`, +/// which are already in ascending key order, so `ksort()` on one is a genuine no-op, as is +/// either sort over a statically empty indexed array. `krsort()` over a non-empty indexed +/// array is rejected instead of silently leaving the receiver untouched, because that +/// storage has no room for a descending key order. pub(super) fn lower_array_key_sort( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, - helper: &str, + order: KeySortOrder, ) -> Result<()> { super::super::ensure_arg_count(inst, name, 1)?; let array = expect_operand(inst, 0)?; - require_array_key_sort_type(ctx.value_php_type(array)?, name)?; - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.load_value_to_reg(array, "x0")?; + match ctx.value_php_type(array)?.codegen_repr() { + PhpType::AssocArray { .. } => { + let helper = match order { + KeySortOrder::Ascending => "__rt_hash_ksort", + KeySortOrder::Descending => "__rt_hash_krsort", + }; + lower_hash_link_sort(ctx, inst, helper) } - Arch::X86_64 => { - ctx.load_value_to_reg(array, "rdi")?; + PhpType::Array(elem) + if order == KeySortOrder::Ascending + || matches!(elem.codegen_repr(), PhpType::Never | PhpType::Void) => + { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 0x7fff_ffff_ffff_fffe, + ); + store_if_result(ctx, inst) } + PhpType::Array(elem) => Err(CodegenIrError::unsupported(format!( + "{} for indexed array<{:?}>: an indexed array stores its keys as slot \ + positions 0..n-1, so descending key order has no representation; convert the \ + receiver to an associative array (for example with array_reverse($a, true)) \ + before sorting it by key", + name, elem + ))), + other => Err(CodegenIrError::unsupported(format!( + "{} for PHP type {:?}", + name, other + ))), } - abi::emit_call_label(ctx.emitter, helper); - abi::emit_load_int_immediate( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - 0x7fff_ffff_ffff_fffe, - ); - store_if_result(ctx, inst) } /// Returns the indexed-array element type accepted by the selected sort helper. @@ -432,6 +472,10 @@ pub(super) fn indexed_sort_element_type(ty: PhpType, name: &str, allow_strings: /// String elements are rejected here exactly as before — their multi-word /// descriptors are not permuted by the 8-byte slot sorter — so they keep /// producing a clear unsupported-feature error rather than a corrupt sort. +/// String elements are 16-byte `[ptr][len]` descriptors, so they are routed to +/// the dedicated `__rt_usort_str` slot permuter instead; only `usort` accepts +/// them because it is the sort that renumbers keys, which an indexed array +/// without key storage can represent exactly. pub(super) fn user_sort_element_type(ty: PhpType, name: &str) -> Result { match ty.codegen_repr() { PhpType::Array(elem) => { @@ -443,7 +487,8 @@ pub(super) fn user_sort_element_type(ty: PhpType, name: &str) -> Result | PhpType::Never | PhpType::Mixed | PhpType::Object(_) - ) { + ) || (elem == PhpType::Str && name == "usort") + { return Ok(elem); } Err(CodegenIrError::unsupported(format!( @@ -458,14 +503,17 @@ pub(super) fn user_sort_element_type(ty: PhpType, name: &str) -> Result } } -/// Verifies key-sort helpers only receive array-like PHP values. -pub(super) fn require_array_key_sort_type(ty: PhpType, name: &str) -> Result<()> { - match ty.codegen_repr() { - PhpType::Array(_) | PhpType::AssocArray { .. } => Ok(()), - other => Err(CodegenIrError::unsupported(format!( - "{} for PHP type {:?}", - name, other - ))), + +/// Returns the user-sort runtime helper matching an indexed array's slot width. +/// +/// String elements occupy 16-byte `[ptr][len]` slots and must be permuted by +/// `__rt_usort_str`; every other supported element kind is a single 8-byte +/// payload handled by `__rt_usort`. +fn user_sort_runtime_label(elem_ty: &PhpType) -> &'static str { + if elem_ty.codegen_repr() == PhpType::Str { + "__rt_usort_str" + } else { + "__rt_usort" } } @@ -483,3 +531,10 @@ pub(super) fn ensure_unique_sort_source(ctx: &mut FunctionContext<'_>, array: Va ctx.store_result_value(array) } +/// Splits a shared hash table before a sort helper relinks its iteration order in place. +pub(super) fn ensure_unique_hash_sort_source(ctx: &mut FunctionContext<'_>, array: ValueId) -> Result<()> { + let array_arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + ctx.load_value_to_reg(array, array_arg_reg)?; + abi::emit_call_label(ctx.emitter, "__rt_hash_ensure_unique"); + ctx.store_result_value(array) +} diff --git a/src/codegen/lower_inst/builtins/arrays/unshift.rs b/src/codegen/lower_inst/builtins/arrays/unshift.rs index 6c77de2d23..a931cbe13a 100644 --- a/src/codegen/lower_inst/builtins/arrays/unshift.rs +++ b/src/codegen/lower_inst/builtins/arrays/unshift.rs @@ -7,6 +7,15 @@ //! //! Key details: //! - Mutates the caller-visible array after copy-on-write splitting. +//! - Accepts PHP's full variadic argument list: `array_unshift($a)` (no values) reads the +//! current length, and N values are prepended one at a time in REVERSE source order so the +//! single-slot runtime helper reproduces PHP's `[v0, v1, …, old…]` result. +//! - Grows the payload before every prepend: `__rt_array_unshift` shifts slots and bumps the +//! length without a capacity check, so a full array would write past its allocation. +//! - The possibly-relocated receiver is published back through `ReceiverPlace`, so a by-reference +//! PARAMETER (read with `load_ref_cell`) reaches the caller's storage too. Without it the +//! caller kept a pointer into the buffer `__rt_array_grow` had already freed, and a receiver +//! with no writable slot at all is now refused instead of silently dropped. //! - Returns the new indexed-array length as PHP `int`. //! - Supports integer and boolean indexed payloads, matching the existing 8-byte helper. @@ -17,27 +26,75 @@ use crate::codegen::{CodegenIrError, Result}; use crate::ir::{Instruction, ValueId}; use crate::types::PhpType; +use super::super::super::receiver_place::ReceiverPlace; use super::super::super::{expect_operand, store_if_result}; -/// Lowers `array_unshift()` by ensuring uniqueness, prepending one scalar value, and returning count. +/// Lowers `array_unshift()` by ensuring uniqueness, prepending every value, and returning count. +/// +/// The operand list is `[array, value…]` with any number of trailing values, matching PHP's +/// `array_unshift(array &$array, mixed ...$values)`. Values are prepended last-to-first so the +/// one-slot `__rt_array_unshift` helper leaves them in source order at the front of the array. pub(super) fn lower_array_unshift(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::super::ensure_arg_count(inst, "array_unshift", 2)?; + if inst.operands.is_empty() { + return Err(CodegenIrError::invalid_module( + "array_unshift expected at least 1 arg, got 0".to_string(), + )); + } let array = expect_operand(inst, 0)?; - let value = expect_operand(inst, 1)?; let elem_ty = array_unshift_element_type(ctx.value_php_type(array)?)?; - let value_ty = ctx.value_php_type(value)?.codegen_repr(); - require_array_unshift_value_type(&elem_ty, &value_ty)?; + for index in 1..inst.operands.len() { + let value = expect_operand(inst, index)?; + let value_ty = ctx.value_php_type(value)?.codegen_repr(); + require_array_unshift_value_type(&elem_ty, &value_ty)?; + } require_array_unshift_result_type(&inst.result_php_type.codegen_repr())?; - let source_local = super::source_load_local_slot(ctx, array)?; - ensure_unique_array_unshift_source(ctx, array)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, array)?; + if inst.operands.len() > 1 { + let receiver = ReceiverPlace::resolve(ctx, array)?; + // Every prepend can reach `__rt_array_grow`, and a grown array lives somewhere else, so + // a receiver with nowhere to publish the new pointer must be refused rather than left + // pointing at freed storage. + receiver.require_writable("array_unshift")?; + ensure_unique_array_unshift_source(ctx, array)?; + // Reverse source order: prepending v_last first and v_first last leaves the values in + // PHP's `[v_first, …, v_last, old…]` layout. Growth is re-checked per element because + // `__rt_array_grow` only guarantees room for one more slot at a time. + for index in (1..inst.operands.len()).rev() { + let value = expect_operand(inst, index)?; + ensure_array_unshift_capacity(ctx, array)?; + match ctx.emitter.target.arch { + Arch::AArch64 => lower_array_unshift_aarch64(ctx, array, value)?, + Arch::X86_64 => lower_array_unshift_x86_64(ctx, array, value)?, + } + } + receiver.store_back_value(ctx, array)?; } + // The helper already returns the running count, but the local-slot write-back above may + // clobber the result register, and the value-less form never calls the helper at all. + // Re-reading the logical length after every mutation covers both cases identically. + load_array_unshift_length_to_result(ctx, array)?; + store_if_result(ctx, inst) +} + +/// Materializes the indexed array's logical length into the result register. +/// +/// The length lives in the first payload word, so this is the post-mutation `int` count PHP +/// returns from `array_unshift()`. It is read after every prepend and after the local-slot +/// write-back, so no earlier call's return register has to survive. +fn load_array_unshift_length_to_result( + ctx: &mut FunctionContext<'_>, + array: ValueId, +) -> Result<()> { match ctx.emitter.target.arch { - Arch::AArch64 => lower_array_unshift_aarch64(ctx, array, value)?, - Arch::X86_64 => lower_array_unshift_x86_64(ctx, array, value)?, + Arch::AArch64 => { + ctx.load_value_to_reg(array, "x0")?; + ctx.emitter.instruction("ldr x0, [x0]"); // read the indexed-array logical length as the int result + } + Arch::X86_64 => { + ctx.load_value_to_reg(array, "rax")?; + ctx.emitter.instruction("mov rax, QWORD PTR [rax]"); // read the indexed-array logical length as the int result + } } - store_if_result(ctx, inst) + Ok(()) } /// Returns the supported element payload type for an indexed-array `array_unshift()`. @@ -99,6 +156,43 @@ fn ensure_unique_array_unshift_source(ctx: &mut FunctionContext<'_>, array: Valu ctx.store_result_value(array) } +/// Guarantees the unique indexed array has room for the extra `array_unshift()` slot. +/// +/// `__rt_array_unshift` shifts every live payload one slot to the right and increments the +/// logical length, but it never checks capacity. On a full array that wrote one element past +/// the payload into the neighbouring heap block and left `length > capacity`, so the next +/// copy-on-write split (`__rt_array_clone_shallow` allocates `capacity` slots and copies +/// `length` slots) both overflowed the clone and read back adjacent heap header words as PHP +/// values. `__rt_array_grow` at least doubles capacity, so one conditional growth is always +/// enough for the single prepended element — which is why a multi-value call re-runs this +/// check before every individual prepend. It returns a possibly-relocated pointer, which the +/// caller stores back into the array value before the local-slot write-back runs. +fn ensure_array_unshift_capacity(ctx: &mut FunctionContext<'_>, array: ValueId) -> Result<()> { + let done_label = ctx.next_label("array_unshift_capacity_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.load_value_to_reg(array, "x0")?; + ctx.emitter.instruction("ldr x9, [x0]"); // load the current indexed-array logical length + ctx.emitter.instruction("ldr x10, [x0, #8]"); // load the current indexed-array slot capacity + ctx.emitter.instruction("cmp x9, x10"); // does the prepended element still fit inside the payload? + ctx.emitter.instruction(&format!("b.lt {}", done_label)); // a spare slot means the shift stays inside the allocation + abi::emit_call_label(ctx.emitter, "__rt_array_grow"); + ctx.store_result_value(array)?; + } + Arch::X86_64 => { + ctx.load_value_to_reg(array, "rdi")?; + ctx.emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the current indexed-array logical length + ctx.emitter.instruction("mov r11, QWORD PTR [rdi + 8]"); // load the current indexed-array slot capacity + ctx.emitter.instruction("cmp r10, r11"); // does the prepended element still fit inside the payload? + ctx.emitter.instruction(&format!("jb {}", done_label)); // a spare slot means the shift stays inside the allocation + abi::emit_call_label(ctx.emitter, "__rt_array_grow"); + ctx.store_result_value(array)?; + } + } + ctx.emitter.label(&done_label); + Ok(()) +} + /// Emits the AArch64 `array_unshift()` runtime call for scalar indexed arrays. fn lower_array_unshift_aarch64( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/count_empty.rs b/src/codegen/lower_inst/builtins/count_empty.rs index 05143e5b57..cd56876833 100644 --- a/src/codegen/lower_inst/builtins/count_empty.rs +++ b/src/codegen/lower_inst/builtins/count_empty.rs @@ -16,9 +16,13 @@ use super::*; /// (delegates to `__rt_mixed_count`), and Countable Object (calls the object's `count` /// method via intrinsic or dynamic dispatch). pub(crate) fn lower_count(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - ensure_arg_count(inst, "count", 1)?; + ensure_arg_count_between(inst, "count", 1, 2)?; let value = expect_operand(inst, 0)?; let ty = ctx.value_php_type(value)?.codegen_repr(); + if inst.operands.len() == 2 { + require_recursive_count_is_flat(&ty)?; + emit_count_mode_guard(ctx, expect_operand(inst, 1)?)?; + } match ty { PhpType::Array(_) | PhpType::AssocArray { .. } => { ctx.load_value_to_result(value)?; @@ -280,3 +284,79 @@ pub(in crate::codegen::lower_inst) fn invert_bool_result(ctx: &mut FunctionConte } } +/// php-src's verbatim `ValueError` wording for an unknown `count()` mode. +const COUNT_MODE_MESSAGE: &str = + "count(): Argument #2 ($mode) must be either COUNT_NORMAL or COUNT_RECURSIVE"; + +/// Materializes `count()`'s `$mode` and raises PHP's `ValueError` for anything else. +/// +/// PHP accepts only `COUNT_NORMAL` (`0`) and `COUNT_RECURSIVE` (`1`) and raises a catchable +/// `ValueError` otherwise, so the guard runs before the receiver is even loaded. `$mode` can be +/// a runtime value, which is why the check is emitted here instead of in the checker. +fn emit_count_mode_guard(ctx: &mut FunctionContext<'_>, mode: ValueId) -> Result<()> { + match ctx.load_value_to_result(mode)?.codegen_repr() { + PhpType::Int | PhpType::Bool => {} + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + } + PhpType::Float => abi::emit_float_result_to_int_result(ctx.emitter), + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, mode)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "count mode for PHP type {:?}", + other + ))) + } + } + let mode_reg = abi::int_result_reg(ctx.emitter); + super::exceptions::emit_value_error_unless( + ctx, + super::exceptions::ValueGuard::SignedInRange(mode_reg, 0, 1), + COUNT_MODE_MESSAGE, + ); + Ok(()) +} + +/// Rejects a `count($value, $mode)` receiver whose `COUNT_RECURSIVE` total is not the flat count. +/// +/// `COUNT_RECURSIVE` adds the size of every nested array, so it only equals the flat count when +/// the receiver provably cannot hold one. elephc's INDEXED arrays store their payload untagged +/// (`[length][capacity][elem_size][elements...]`), so a runtime walk cannot tell an +/// `array` slot from an `array>` slot; recursing anyway would either read +/// integers as pointers or silently undercount. Until the array header carries an element tag, +/// a receiver that CAN nest is refused with an explicit diagnostic instead. +fn require_recursive_count_is_flat(ty: &PhpType) -> Result<()> { + let element = match ty { + PhpType::Array(elem) => elem.codegen_repr(), + PhpType::AssocArray { value, .. } => value.codegen_repr(), + // php-src ignores `$mode` entirely for Countable objects: it calls `count()` and + // returns that value, so every object receiver is already exact. + PhpType::Object(_) => return Ok(()), + other => { + return Err(CodegenIrError::unsupported(format!( + "count() with an explicit $mode for PHP type {:?} (COUNT_RECURSIVE needs a \ + statically known element type)", + other + ))) + } + }; + if matches!( + element, + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Iterable + | PhpType::Object(_) + ) { + return Err(CodegenIrError::unsupported(format!( + "count() with an explicit $mode over an array of {:?} (COUNT_RECURSIVE over nested \ + containers needs a runtime element tag in the array header)", + element + ))); + } + Ok(()) +} diff --git a/src/codegen/lower_inst/builtins/debug.rs b/src/codegen/lower_inst/builtins/debug.rs index 054d7496c2..414f550009 100644 --- a/src/codegen/lower_inst/builtins/debug.rs +++ b/src/codegen/lower_inst/builtins/debug.rs @@ -18,7 +18,12 @@ //! the object tag, the same entry point a nested object reaches, so top-level //! and nested dumps share one renderer. The class name, per-property body and //! `*RECURSION*` guard all come from `codegen_support::runtime::io:: -//! var_dump_object`. KNOWN DIVERGENCE: no `#id` handle — see that module. +//! var_dump_object`. An ENUM case is intercepted inside that shared renderer +//! and printed as `enum(E::C)`, so this file needs no enum-specific arm. +//! - `print_r` of an object works the same way: `__rt_print_r_object` in +//! `codegen_support::runtime::objects::print_r_object` owns the header, the +//! parenthesized body and the recursion guard, and is reached both from here +//! (base indent 0) and from the tag-6 branch of `__rt_print_r_value`. use crate::codegen::abi; use crate::codegen::data_section::DataSection; @@ -260,6 +265,10 @@ fn emit_print_r_loaded_value(ctx: &mut FunctionContext<'_>, ty: &PhpType) -> Res emit_write_literal(ctx, b"Array\n"); Ok(()) } + PhpType::Object(_) => { + emit_print_r_object(ctx); + Ok(()) + } PhpType::Mixed | PhpType::Union(_) => { emit_print_r_mixed(ctx); Ok(()) @@ -335,6 +344,27 @@ fn emit_print_r_mixed(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_print_r_value"); } +/// Emits `print_r` output for an object pointer in the integer result register. +/// +/// Hands the instance to `__rt_print_r_object` with a base indent of 0 — the SAME +/// entry point a nested object reaches from the array, hash and object walkers, so +/// a top-level render and a render at depth cannot drift apart. That helper owns +/// the whole layout: the `ClassName Object` header (or PHP's `ClassName Enum[:t]` +/// for an enum case), the `(` / `)` lines, the per-property body and the +/// `*RECURSION*` guard. +fn emit_print_r_object(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x1, #0"); // base indent = 0 for the top-level object + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // object pointer → SysV first argument register + ctx.emitter.instruction("mov esi, 0"); // base indent = 0 for the top-level object + } + } + abi::emit_call_label(ctx.emitter, "__rt_print_r_object"); +} + /// Emits `var_dump` output for a boxed Mixed payload in the integer result register. fn emit_var_dump_mixed(ctx: &mut FunctionContext<'_>) -> Result<()> { let int_case = ctx.next_label("var_dump_mixed_int"); @@ -458,7 +488,7 @@ fn emit_var_dump_int_payload(ctx: &mut FunctionContext<'_>) { /// Emits `var_dump` output for a float payload in the floating result register. fn emit_var_dump_float(ctx: &mut FunctionContext<'_>) -> Result<()> { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); - abi::emit_call_label(ctx.emitter, "__rt_ftoa"); + abi::emit_call_label(ctx.emitter, "__rt_ftoa_repr"); abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); emit_write_literal(ctx, b"float("); abi::emit_pop_reg_pair(ctx.emitter, ptr_reg, len_reg); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index e90d19d2cc..5c9925ee33 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -32,7 +32,6 @@ use super::super::super::context::FunctionContext; use super::super::{ expect_data, expect_global_name, expect_operand, function_signature_from_eir, store_if_result, }; -use super::ensure_arg_count; const EVAL_STATUS_PARSE_ERROR: i64 = 1; const EVAL_STATUS_UNCAUGHT_THROWABLE: i64 = 3; diff --git a/src/codegen/lower_inst/builtins/eval/calls.rs b/src/codegen/lower_inst/builtins/eval/calls.rs index 5ea82525f5..fc7606c1bd 100644 --- a/src/codegen/lower_inst/builtins/eval/calls.rs +++ b/src/codegen/lower_inst/builtins/eval/calls.rs @@ -11,7 +11,7 @@ use super::*; /// Lowers `eval($code)` through internal EIR AOT or the bridge ABI and leaves its result in registers. pub(in crate::codegen::lower_inst::builtins) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "eval", 1)?; + super::super::ensure_arg_count(inst, "eval", 1)?; if let Some(fragment) = eval_literal_fragment(ctx, inst)? { if lower_eval_literal_eir_function(ctx, inst, &fragment)? { return Ok(()); diff --git a/src/codegen/lower_inst/builtins/eval/dynamic_calls.rs b/src/codegen/lower_inst/builtins/eval/dynamic_calls.rs index 5f7bbbea99..f7e43725ef 100644 --- a/src/codegen/lower_inst/builtins/eval/dynamic_calls.rs +++ b/src/codegen/lower_inst/builtins/eval/dynamic_calls.rs @@ -59,7 +59,7 @@ pub(in crate::codegen::lower_inst::builtins) fn lower_eval_function_call_array( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "eval function call array", 1)?; + super::super::ensure_arg_count(inst, "eval function call array", 1)?; let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); let arg_array = expect_operand(inst, 0)?; abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); diff --git a/src/codegen/lower_inst/builtins/eval/scope_access.rs b/src/codegen/lower_inst/builtins/eval/scope_access.rs index 8d65b70279..8b2b93d0a1 100644 --- a/src/codegen/lower_inst/builtins/eval/scope_access.rs +++ b/src/codegen/lower_inst/builtins/eval/scope_access.rs @@ -14,7 +14,7 @@ pub(in crate::codegen::lower_inst::builtins) fn lower_eval_scope_get( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "eval scope get", 1)?; + super::super::ensure_arg_count(inst, "eval scope get", 1)?; let scope = expect_operand(inst, 0)?; let name = eval_scope_instruction_name(ctx, inst)?; abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); @@ -31,7 +31,7 @@ pub(in crate::codegen::lower_inst::builtins) fn lower_eval_scope_set( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "eval scope set", 2)?; + super::super::ensure_arg_count(inst, "eval scope set", 2)?; let scope = expect_operand(inst, 0)?; let value = expect_operand(inst, 1)?; let name = eval_scope_instruction_name(ctx, inst)?; diff --git a/src/codegen/lower_inst/builtins/io.rs b/src/codegen/lower_inst/builtins/io.rs index acb248e5dd..d4979ac722 100644 --- a/src/codegen/lower_inst/builtins/io.rs +++ b/src/codegen/lower_inst/builtins/io.rs @@ -17,6 +17,7 @@ use crate::types::PhpType; use super::super::super::context::FunctionContext; use super::{expect_operand, load_value_to_first_int_arg, store_if_result}; +use super::super::resolve_int_operand_to_result; const STREAM_METADATA_SLOT: usize = 14; const STREAM_WRAPPER_UNLINK_SLOT: usize = 15; @@ -168,3 +169,43 @@ pub(crate) use stat_ops::{ pub(super) use boxing_helpers::box_owned_string_or_false_result; pub(super) use resource_handles::load_stream_fd_to_result; pub(super) use string_validation::load_string_to_result; + +/// Emits a literal `file_get_contents("phar://...")` payload through compile-time PHAR extraction. +/// +/// The extracted bytes live in read-only `.data`, so a following `$offset`/`$length` window — which +/// trims its input in place and frees a failed read — would move and free a rodata pointer. +/// `persist` therefore copies the entry into an owned heap string before the window runs. +fn emit_literal_phar_file_get_contents_bytes( + ctx: &mut FunctionContext<'_>, + path: &str, + persist: bool, +) { + match crate::codegen::phar_stream::extract_phar_entry(path) { + Some(payload) => { + let (symbol, len) = ctx.data.add_string(&payload); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_symbol_address(ctx.emitter, "x1", &symbol); + ctx.emitter.instruction(&format!("mov x2, #{}", len)); // embedded phar entry byte length + } + Arch::X86_64 => { + abi::emit_symbol_address(ctx.emitter, "rax", &symbol); + ctx.emitter.instruction(&format!("mov rdx, {}", len)); // embedded phar entry byte length + } + } + if persist { + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + } + } + None => match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x1, #0"); // null string pointer asks the boxer for PHP false + ctx.emitter.instruction("mov x2, #0"); // clear the unused failure length + } + Arch::X86_64 => { + ctx.emitter.instruction("xor eax, eax"); // null string pointer asks the boxer for PHP false + ctx.emitter.instruction("xor edx, edx"); // clear the unused failure length + } + }, + } +} diff --git a/src/codegen/lower_inst/builtins/io/fopen_phar.rs b/src/codegen/lower_inst/builtins/io/fopen_phar.rs index d5f8d27cef..34848a1273 100644 --- a/src/codegen/lower_inst/builtins/io/fopen_phar.rs +++ b/src/codegen/lower_inst/builtins/io/fopen_phar.rs @@ -9,40 +9,7 @@ use super::*; -/// Lowers a literal `file_get_contents("phar://...")` through compile-time PHAR extraction. -pub(super) fn lower_literal_phar_file_get_contents( - ctx: &mut FunctionContext<'_>, - inst: &Instruction, - path: &str, -) -> Result<()> { - match crate::codegen::phar_stream::extract_phar_entry(path) { - Some(payload) => { - let (symbol, len) = ctx.data.add_string(&payload); - match ctx.emitter.target.arch { - Arch::AArch64 => { - abi::emit_symbol_address(ctx.emitter, "x1", &symbol); - ctx.emitter.instruction(&format!("mov x2, #{}", len)); // embedded phar entry byte length - } - Arch::X86_64 => { - abi::emit_symbol_address(ctx.emitter, "rax", &symbol); - ctx.emitter.instruction(&format!("mov rdx, {}", len)); // embedded phar entry byte length - } - } - } - None => match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.emitter.instruction("mov x1, #0"); // null string pointer asks the boxer for PHP false - ctx.emitter.instruction("mov x2, #0"); // clear the unused failure length - } - Arch::X86_64 => { - ctx.emitter.instruction("xor eax, eax"); // null string pointer asks the boxer for PHP false - ctx.emitter.instruction("xor edx, edx"); // clear the unused failure length - } - }, - } - box_owned_string_or_false_result(ctx, "fgc_phar"); - store_if_result(ctx, inst) -} + /// Lowers a literal read-mode `fopen("phar://...", ...)` through embedded entry bytes. pub(super) fn lower_literal_phar_fopen_read( diff --git a/src/codegen/lower_inst/builtins/io/host_directory_process.rs b/src/codegen/lower_inst/builtins/io/host_directory_process.rs index 2f7256b0b6..49aa075cfc 100644 --- a/src/codegen/lower_inst/builtins/io/host_directory_process.rs +++ b/src/codegen/lower_inst/builtins/io/host_directory_process.rs @@ -325,8 +325,43 @@ pub(crate) fn lower_fsockopen(ctx: &mut FunctionContext<'_>, inst: &Instruction) } /// Lowers `file(path)` through the target-aware runtime line-array helper. +/// Lowers `file(path, flags)` through the target-aware runtime line-array helper. +/// +/// PHP's `$flags` bitmask is an ordinary run-time integer, so it needs no literal: the helper +/// applies `FILE_IGNORE_NEW_LINES` / `FILE_SKIP_EMPTY_LINES` while it produces each line. The +/// flags are resolved and spilled BEFORE the path, because coercing a non-string path calls a +/// conversion helper that clobbers the caller-saved register the flags would otherwise sit in. pub(crate) fn lower_file(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - lower_unary_path_array(ctx, inst, "file", "__rt_file") + ensure_arg_count_between(inst, "file", 1, 2)?; + let path = expect_operand(inst, 0)?; + match inst.operands.get(1).copied() { + None => { + load_string_to_result(ctx, path, "file")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #0"); // no $flags argument: request PHP's default behavior + } + Arch::X86_64 => { + ctx.emitter.instruction("xor edi, edi"); // no $flags argument: request PHP's default behavior + } + } + } + Some(flags) => { + resolve_int_operand_to_result(ctx, flags, "file flags")?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + load_string_to_result(ctx, path, "file")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x0"); // restore the resolved $flags bitmask into the first runtime argument + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "rdi"); // restore the resolved $flags bitmask into the first runtime argument + } + } + } + } + abi::emit_call_label(ctx.emitter, "__rt_file"); + store_if_result(ctx, inst) } /// Lowers `realpath(path)` and boxes the owned runtime string-or-false result. diff --git a/src/codegen/lower_inst/builtins/io/phar_read.rs b/src/codegen/lower_inst/builtins/io/phar_read.rs index 8bf86173ab..3ce558885b 100644 --- a/src/codegen/lower_inst/builtins/io/phar_read.rs +++ b/src/codegen/lower_inst/builtins/io/phar_read.rs @@ -10,24 +10,55 @@ use super::*; /// Lowers `file_get_contents(path)` and boxes the runtime string-or-false result. +/// php-src's `ValueError` for a negative `file_get_contents()` `$length`. +const FILE_GET_CONTENTS_NEGATIVE_LENGTH_MESSAGE: &str = + "file_get_contents(): Argument #5 ($length) must be greater than or equal to 0"; + +/// Lowers `file_get_contents(path, use_include_path?, context?, offset?, length?)` and boxes the +/// runtime string-or-false result. +/// +/// The full read runs first and `$offset`/`$length` then trim the owned buffer in place through +/// `__rt_file_get_contents_range`, which reproduces what PHP's seek-then-read produces for a +/// seekable stream while keeping the allocation and the copy bounded by the same byte count. +/// The negative-`$length` `ValueError` is raised BEFORE the read, exactly like php-src, so a +/// missing file plus a negative length still throws instead of warning. pub(crate) fn lower_file_get_contents( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::super::ensure_arg_count(inst, "file_get_contents", 1)?; + ensure_arg_count_between(inst, "file_get_contents", 1, 5)?; + require_absent_stream_context(ctx, inst, 2, "file_get_contents")?; + let range = FileReadRange::from_operands(ctx, inst, 3, 4)?; + range.emit_negative_length_guard(ctx, FILE_GET_CONTENTS_NEGATIVE_LENGTH_MESSAGE)?; + emit_file_get_contents_bytes(ctx, inst, range.is_active())?; + range.emit(ctx, "file_get_contents")?; + box_owned_string_or_false_result(ctx, "fgc"); + store_if_result(ctx, inst) +} + +/// Emits the unsliced `file_get_contents()` read, leaving the bytes in the string result registers. +/// +/// `persist_literal_bytes` is set when a `$offset`/`$length` window follows: the literal `phar://` +/// shortcut answers with a pointer into read-only `.data`, which the in-place range trim must never +/// move or free, so those bytes are copied into an owned string first. +fn emit_file_get_contents_bytes( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + persist_literal_bytes: bool, +) -> Result<()> { let path = expect_operand(inst, 0)?; let path_literal = optional_const_string_operand(ctx, path)?; if let Some(path_literal) = path_literal.as_deref() { if path_literal.starts_with("phar://") { - return lower_literal_phar_file_get_contents(ctx, inst, path_literal); + emit_literal_phar_file_get_contents_bytes(ctx, path_literal, persist_literal_bytes); + return Ok(()); } if path_literal == "php://input" { // file_get_contents('php://input'): under --web `__rt_php_input` copies // the captured request body into an owned string; in a non-web build it // returns a null pointer so the result boxes to PHP false. abi::emit_call_label(ctx.emitter, "__rt_php_input"); - box_owned_string_or_false_result(ctx, "fgc"); - return store_if_result(ctx, inst); + return Ok(()); } } if path_literal.is_none() { @@ -35,8 +66,213 @@ pub(crate) fn lower_file_get_contents( } load_string_to_result(ctx, path, "file_get_contents filename")?; abi::emit_call_label(ctx.emitter, "__rt_file_get_contents_maybe_url"); - box_owned_string_or_false_result(ctx, "fgc"); - store_if_result(ctx, inst) + Ok(()) +} + +/// Rejects a non-null `$context` argument instead of silently ignoring the stream context. +/// +/// elephc has no stream-context plumbing on the read path, so honoring a real context is +/// impossible. An omitted argument, a literal `null`, and the registry's `null` default all +/// materialize a statically null operand and are accepted; anything else is a compile error +/// naming the parameter rather than a read that quietly drops the caller's options. +fn require_absent_stream_context( + ctx: &FunctionContext<'_>, + inst: &Instruction, + index: usize, + name: &str, +) -> Result<()> { + let Some(context) = inst.operands.get(index).copied() else { + return Ok(()); + }; + if operand_is_statically_null(ctx, context)? { + return Ok(()); + } + Err(CodegenIrError::unsupported(format!( + "{}() $context argument: elephc cannot honor a stream context on this read, pass null", + name + ))) +} + +/// Reports whether an operand is the PHP `null` value at compile time. +/// +/// Covers both spellings the argument planner produces: a `ConstNull` instruction for a literal +/// `null` argument or a filled-in `null` default, and a `Void`-typed value for an operand the +/// planner materialized without a concrete constant. +fn operand_is_statically_null(ctx: &FunctionContext<'_>, value: ValueId) -> Result { + if matches!(ctx.value_php_type(value)?.codegen_repr(), PhpType::Void) { + return Ok(true); + } + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(false); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + Ok(inst_ref.op == Op::ConstNull) +} + +/// The `$offset`/`$length` window a one-shot file read applies to the bytes it produced. +/// +/// Both operands are optional: `None` means the PHP call omitted the argument, and a statically +/// absent `$length` (omitted or the `null` default) means "to the end of the data". +struct FileReadRange { + /// The `$offset` operand, when the call passed one. + offset: Option, + /// The `$length` operand, when the call passed one. + length: Option, + /// Whether `$length` is known at compile time to be absent (omitted or literal `null`). + length_statically_absent: bool, +} + +impl FileReadRange { + /// Reads the optional `$offset`/`$length` operands at the given positions. + fn from_operands( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + offset_index: usize, + length_index: usize, + ) -> Result { + let offset = inst.operands.get(offset_index).copied(); + let length = inst.operands.get(length_index).copied(); + let length_statically_absent = match length { + None => true, + Some(length) => matches!(ctx.value_php_type(length)?.codegen_repr(), PhpType::Void), + }; + Ok(Self { + offset, + length, + length_statically_absent, + }) + } + + /// Reports whether any trimming has to happen at run time. + /// + /// A call that passed neither argument keeps the untouched read result, so no range helper + /// call is emitted at all and the 1-argument lowering is byte-for-byte what it was. + fn is_active(&self) -> bool { + self.offset.is_some() || self.length.is_some() + } + + /// Raises php-src's negative-`$length` `ValueError` before the read is attempted. + /// + /// A statically absent `$length` needs no guard. A boxed `Mixed` `null` casts to `0`, which + /// passes the guard, so a runtime `null` still reads to the end instead of throwing. + fn emit_negative_length_guard( + &self, + ctx: &mut FunctionContext<'_>, + message: &str, + ) -> Result<()> { + if self.length_statically_absent { + return Ok(()); + } + let length = self.length.expect("length operand present"); + resolve_int_operand_to_result(ctx, length, "file read length")?; + let reg = abi::int_result_reg(ctx.emitter); + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedAtLeast(reg, 0), + message, + ); + Ok(()) + } + + /// Trims the string currently in the string result registers to the requested window. + /// + /// The read result is spilled across the integer resolutions because unboxing a `Mixed` + /// argument calls `__rt_mixed_cast_int`, which clobbers the caller-saved registers the + /// pointer/length pair lives in. + fn emit(&self, ctx: &mut FunctionContext<'_>, name: &str) -> Result<()> { + if !self.is_active() { + return Ok(()); + } + let (text_ptr, text_len) = abi::string_result_regs(ctx.emitter); + abi::emit_push_reg_pair(ctx.emitter, text_ptr, text_len); + self.resolve_offset(ctx, name)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + self.resolve_length_present(ctx)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + self.resolve_length(ctx, name)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x4, x0"); // pass the resolved byte length as the range helper's fourth argument + abi::emit_pop_reg(ctx.emitter, "x5"); // restore the length-present flag into the fifth range argument + abi::emit_pop_reg(ctx.emitter, "x3"); // restore the resolved byte offset into the third range argument + abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rsi, rax"); // pass the resolved byte length as the range helper's fourth argument + abi::emit_pop_reg(ctx.emitter, "rcx"); // restore the length-present flag into the fifth range argument + abi::emit_pop_reg(ctx.emitter, "rdi"); // restore the resolved byte offset into the third range argument + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + } + } + abi::emit_call_label(ctx.emitter, "__rt_file_get_contents_range"); + Ok(()) + } + + /// Resolves `$offset` into the integer result register, defaulting an omitted one to `0`. + fn resolve_offset(&self, ctx: &mut FunctionContext<'_>, name: &str) -> Result<()> { + match self.offset { + None => { + let reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, reg, 0); + Ok(()) + } + Some(offset) => { + resolve_int_operand_to_result(ctx, offset, &format!("{} offset", name)) + } + } + } + + /// Resolves `$length` into the integer result register, using `0` for an absent one. + fn resolve_length(&self, ctx: &mut FunctionContext<'_>, name: &str) -> Result<()> { + if self.length_statically_absent { + let reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, reg, 0); + return Ok(()); + } + let length = self.length.expect("length operand present"); + resolve_int_operand_to_result(ctx, length, &format!("{} length", name)) + } + + /// Resolves the length-present flag PHP's `?int $length` needs. + /// + /// `null` means "read to the end", and every real `i64` — including `0` — is a genuine byte + /// count, so the helper cannot recognise the absent case from the length value alone. + fn resolve_length_present(&self, ctx: &mut FunctionContext<'_>) -> Result<()> { + let reg = abi::int_result_reg(ctx.emitter); + if self.length_statically_absent { + abi::emit_load_int_immediate(ctx.emitter, reg, 0); + return Ok(()); + } + let length = self.length.expect("length operand present"); + if !matches!( + ctx.value_php_type(length)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { + abi::emit_load_int_immediate(ctx.emitter, reg, 1); + return Ok(()); + } + ctx.load_value_to_result(length)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #8"); // runtime tag 8 marks a boxed PHP null length argument + ctx.emitter.instruction("cset x0, ne"); // report a length only when the boxed payload is not null + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 8"); // runtime tag 8 marks a boxed PHP null length argument + ctx.emitter.instruction("setne al"); // report a length only when the boxed payload is not null + ctx.emitter.instruction("movzx rax, al"); // widen the length-present flag to a full integer argument word + } + } + Ok(()) + } } /// Publishes bridge/decompressor entry points into runtime slots used by diff --git a/src/codegen/lower_inst/builtins/is_numeric.rs b/src/codegen/lower_inst/builtins/is_numeric.rs index fa6fcb9f54..97e83a1ce2 100644 --- a/src/codegen/lower_inst/builtins/is_numeric.rs +++ b/src/codegen/lower_inst/builtins/is_numeric.rs @@ -1,13 +1,19 @@ //! Purpose: //! Lowers PHP `is_numeric()` for concrete scalar EIR operands. -//! Keeps the byte scanner separate from the builtin dispatcher. +//! Keeps the type dispatch separate from the builtin dispatcher; the string grammar +//! itself lives in the shared runtime scanner. //! //! Called from: //! - `crate::codegen::lower_inst::builtins::lower_language_construct_call()`. //! //! Key details: -//! - The string grammar accepts an optional leading `-`, -//! digits, optional `.`, and at least one digit overall. +//! - The string case delegates to `__rt_str_to_number`, whose numeric flag is +//! `__rt_php_num_scan`'s implementation of PHP's `is_numeric_string()` grammar: +//! optional leading/trailing PHP whitespace, optional sign, a mantissa with at least +//! one digit (`12`, `.5`, `5.`), and an exponent only when a digit follows it. Hex, +//! underscore separators, `INF` and `NAN` are NOT numeric. Sharing that one scanner is +//! what keeps `is_numeric($s)` and `(float) $s` / `(int) $s` consistent with each other +//! and with the compile-time folder in `crate::optimize::fold::compare`. use crate::codegen::abi; use crate::codegen::platform::Arch; @@ -104,139 +110,11 @@ fn emit_static_bool(ctx: &mut FunctionContext<'_>, value: bool) { ); } -/// Emits the legacy ASCII numeric-string scan used by `is_numeric()`. +/// Emits PHP's numeric-string test for a string in the string-result registers. +/// +/// Delegates to `__rt_str_to_number`, which clips the string to PHP's leading numeric run +/// and reports in the integer result register whether the WHOLE string was numeric. The +/// parsed double it also leaves in the float result register is unused here. fn emit_string_is_numeric(ctx: &mut FunctionContext<'_>) { - let loop_label = ctx.next_label("isnum_loop"); - let dot_label = ctx.next_label("isnum_dot"); - let frac_loop = ctx.next_label("isnum_frac"); - let fail_label = ctx.next_label("isnum_fail"); - let pass_label = ctx.next_label("isnum_pass"); - let end_label = ctx.next_label("isnum_end"); - match ctx.emitter.target.arch { - Arch::AArch64 => emit_string_is_numeric_aarch64( - ctx, - &loop_label, - &dot_label, - &frac_loop, - &fail_label, - &pass_label, - &end_label, - ), - Arch::X86_64 => emit_string_is_numeric_x86_64( - ctx, - &loop_label, - &dot_label, - &frac_loop, - &fail_label, - &pass_label, - &end_label, - ), - } -} - -/// Emits the AArch64 string scan for `is_numeric()`. -fn emit_string_is_numeric_aarch64( - ctx: &mut FunctionContext<'_>, - loop_label: &str, - dot_label: &str, - frac_loop: &str, - fail_label: &str, - pass_label: &str, - end_label: &str, -) { - ctx.emitter.instruction(&format!("cbz x2, {}", fail_label)); // empty strings are not numeric - ctx.emitter.instruction("mov x3, #0"); // initialize the string scan index - ctx.emitter.instruction("mov x5, #0"); // initialize the consumed digit count - ctx.emitter.instruction("ldrb w4, [x1]"); // load the first string byte for sign handling - ctx.emitter.instruction("cmp w4, #45"); // check whether the string starts with '-' - ctx.emitter.instruction(&format!("b.ne {}", loop_label)); // start digit scanning when there is no sign - ctx.emitter.instruction("add x3, x3, #1"); // skip the leading minus sign - ctx.emitter.instruction("cmp x3, x2"); // reject a string that contains only the sign - ctx.emitter.instruction(&format!("b.ge {}", fail_label)); // bare '-' is not numeric - ctx.emitter.label(loop_label); - ctx.emitter.instruction("cmp x3, x2"); // check whether the scan reached the string length - ctx.emitter.instruction(&format!("b.ge {}", pass_label)); // finish after scanning the integer part - ctx.emitter.instruction("ldrb w4, [x1, x3]"); // load the current integer-part byte - ctx.emitter.instruction("cmp w4, #46"); // check whether the byte is '.' - ctx.emitter.instruction(&format!("b.eq {}", dot_label)); // switch to fractional scanning at a dot - ctx.emitter.instruction("sub w6, w4, #48"); // normalize the byte to a candidate decimal digit - ctx.emitter.instruction("cmp w6, #9"); // verify the candidate digit range - ctx.emitter.instruction(&format!("b.hi {}", fail_label)); // non-digit bytes make the string non-numeric - ctx.emitter.instruction("add x5, x5, #1"); // record one consumed digit - ctx.emitter.instruction("add x3, x3, #1"); // advance to the next byte - ctx.emitter.instruction(&format!("b {}", loop_label)); // continue integer-part scanning - ctx.emitter.label(dot_label); - ctx.emitter.instruction("add x3, x3, #1"); // skip the decimal point - ctx.emitter.label(frac_loop); - ctx.emitter.instruction("cmp x3, x2"); // check whether the fractional scan reached the end - ctx.emitter.instruction(&format!("b.ge {}", pass_label)); // finish after scanning the fractional part - ctx.emitter.instruction("ldrb w4, [x1, x3]"); // load the current fractional byte - ctx.emitter.instruction("sub w6, w4, #48"); // normalize the byte to a candidate decimal digit - ctx.emitter.instruction("cmp w6, #9"); // verify the fractional digit range - ctx.emitter.instruction(&format!("b.hi {}", fail_label)); // non-digit fractional bytes make the string non-numeric - ctx.emitter.instruction("add x5, x5, #1"); // record one consumed fractional digit - ctx.emitter.instruction("add x3, x3, #1"); // advance to the next fractional byte - ctx.emitter.instruction(&format!("b {}", frac_loop)); // continue fractional scanning - ctx.emitter.label(pass_label); - ctx.emitter.instruction("cmp x5, #0"); // require at least one digit overall - ctx.emitter.instruction(&format!("b.eq {}", fail_label)); // reject strings like '.' or '-.' - ctx.emitter.instruction("mov x0, #1"); // return true for a numeric-looking string - ctx.emitter.instruction(&format!("b {}", end_label)); // skip the false result path - ctx.emitter.label(fail_label); - ctx.emitter.instruction("mov x0, #0"); // return false for a non-numeric string - ctx.emitter.label(end_label); -} - -/// Emits the x86_64 string scan for `is_numeric()`. -fn emit_string_is_numeric_x86_64( - ctx: &mut FunctionContext<'_>, - loop_label: &str, - dot_label: &str, - frac_loop: &str, - fail_label: &str, - pass_label: &str, - end_label: &str, -) { - ctx.emitter.instruction("test rdx, rdx"); // empty strings are not numeric - ctx.emitter.instruction(&format!("je {}", fail_label)); // branch to failure for an empty string - ctx.emitter.instruction("mov rcx, 0"); // initialize the string scan index - ctx.emitter.instruction("mov r8, 0"); // initialize the consumed digit count - ctx.emitter.instruction("movzx r9d, BYTE PTR [rax]"); // load the first string byte for sign handling - ctx.emitter.instruction("cmp r9d, 45"); // check whether the string starts with '-' - ctx.emitter.instruction(&format!("jne {}", loop_label)); // start digit scanning when there is no sign - ctx.emitter.instruction("add rcx, 1"); // skip the leading minus sign - ctx.emitter.instruction("cmp rcx, rdx"); // reject a string that contains only the sign - ctx.emitter.instruction(&format!("jae {}", fail_label)); // bare '-' is not numeric - ctx.emitter.label(loop_label); - ctx.emitter.instruction("cmp rcx, rdx"); // check whether the scan reached the string length - ctx.emitter.instruction(&format!("jae {}", pass_label)); // finish after scanning the integer part - ctx.emitter.instruction("movzx r9d, BYTE PTR [rax + rcx]"); // load the current integer-part byte - ctx.emitter.instruction("cmp r9d, 46"); // check whether the byte is '.' - ctx.emitter.instruction(&format!("je {}", dot_label)); // switch to fractional scanning at a dot - ctx.emitter.instruction("sub r9d, 48"); // normalize the byte to a candidate decimal digit - ctx.emitter.instruction("cmp r9d, 9"); // verify the candidate digit range - ctx.emitter.instruction(&format!("ja {}", fail_label)); // non-digit bytes make the string non-numeric - ctx.emitter.instruction("add r8, 1"); // record one consumed digit - ctx.emitter.instruction("add rcx, 1"); // advance to the next byte - ctx.emitter.instruction(&format!("jmp {}", loop_label)); // continue integer-part scanning - ctx.emitter.label(dot_label); - ctx.emitter.instruction("add rcx, 1"); // skip the decimal point - ctx.emitter.label(frac_loop); - ctx.emitter.instruction("cmp rcx, rdx"); // check whether the fractional scan reached the end - ctx.emitter.instruction(&format!("jae {}", pass_label)); // finish after scanning the fractional part - ctx.emitter.instruction("movzx r9d, BYTE PTR [rax + rcx]"); // load the current fractional byte - ctx.emitter.instruction("sub r9d, 48"); // normalize the byte to a candidate decimal digit - ctx.emitter.instruction("cmp r9d, 9"); // verify the fractional digit range - ctx.emitter.instruction(&format!("ja {}", fail_label)); // non-digit fractional bytes make the string non-numeric - ctx.emitter.instruction("add r8, 1"); // record one consumed fractional digit - ctx.emitter.instruction("add rcx, 1"); // advance to the next fractional byte - ctx.emitter.instruction(&format!("jmp {}", frac_loop)); // continue fractional scanning - ctx.emitter.label(pass_label); - ctx.emitter.instruction("test r8, r8"); // require at least one digit overall - ctx.emitter.instruction(&format!("je {}", fail_label)); // reject strings like '.' or '-.' - ctx.emitter.instruction("mov rax, 1"); // return true for a numeric-looking string - ctx.emitter.instruction(&format!("jmp {}", end_label)); // skip the false result path - ctx.emitter.label(fail_label); - ctx.emitter.instruction("mov rax, 0"); // return false for a non-numeric string - ctx.emitter.label(end_label); + abi::emit_call_label(ctx.emitter, "__rt_str_to_number"); } diff --git a/src/codegen/lower_inst/builtins/math.rs b/src/codegen/lower_inst/builtins/math.rs index 0b80b80ecb..176bddc678 100644 --- a/src/codegen/lower_inst/builtins/math.rs +++ b/src/codegen/lower_inst/builtins/math.rs @@ -22,6 +22,7 @@ use super::{expect_operand, store_if_result}; mod binary; mod libm; +mod min_max_array; mod random; pub(crate) use binary::{lower_fdiv, lower_fmod, lower_intdiv, lower_pow}; @@ -48,18 +49,14 @@ pub(crate) fn lower_abs(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Re emit_float_abs(ctx); PhpType::Float } - PhpType::Int | PhpType::Bool => { - emit_int_abs(ctx); - PhpType::Int - } + PhpType::Int | PhpType::Bool => emit_int_abs_for_result(ctx, inst)?, PhpType::Mixed | PhpType::Union(_) => { abi::emit_call_label(ctx.emitter, "__rt_abs_mixed"); PhpType::Mixed } PhpType::TaggedScalar => { crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); - emit_int_abs(ctx); - PhpType::Int + emit_int_abs_for_result(ctx, inst)? } PhpType::Void | PhpType::Never => { abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); @@ -217,6 +214,9 @@ pub(crate) fn lower_round(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> } /// Lowers numeric `min()` and `max()` over concrete integer-like or float operands. +/// +/// PHP's one-argument form reduces a single array instead of comparing arguments, +/// so it is routed to the dedicated array reduction before the variadic paths. pub(crate) fn lower_min_max( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -228,6 +228,9 @@ pub(crate) fn lower_min_max( min_max_name(want_max) ))); } + if min_max_array::try_lower_single_array(ctx, inst, want_max)? { + return store_if_result(ctx, inst); + } let result_ty = inst .result .map(|value| ctx.value_php_type(value)) @@ -794,6 +797,56 @@ fn emit_float_abs(ctx: &mut FunctionContext<'_>) { } } +/// Emits `abs()` for an integer operand already loaded in the integer result register. +/// +/// Returns the PHP type actually materialized so the caller knows whether a boxing step is +/// still required. When the EIR result type is `Mixed`, the overflowing input is honoured the +/// way reference PHP does it: `abs(PHP_INT_MIN)` has no `int` value, so PHP returns +/// `float(9.2233720368547758E+18)`. `abs($x)` for a negative `$x` is exactly `0 - $x`, so the +/// existing checked-subtraction helper produces the boxed `int`-or-promoted-`float` result +/// with the same overflow rule as `$a - $b`. Non-negative inputs never overflow and are boxed +/// as plain integers. +fn emit_int_abs_for_result( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result { + if !matches!( + inst.result_php_type.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { + emit_int_abs(ctx); + return Ok(PhpType::Int); + } + let result_reg = abi::int_result_reg(ctx.emitter); + let negative_label = ctx.next_label("abs_negative"); + let done_label = ctx.next_label("abs_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("tbnz {}, #63, {}", result_reg, negative_label)); // negative inputs need the overflow-checked negation + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // inspect the sign of the integer operand + ctx.emitter.instruction(&format!("js {}", negative_label)); // negative inputs need the overflow-checked negation + } + } + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&negative_label); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x1, x0"); // pass the negative operand as the checked-subtraction right operand + ctx.emitter.instruction("mov x0, #0"); // abs(x) for x < 0 is 0 - x + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rsi, rax"); // pass the negative operand as the checked-subtraction right operand + ctx.emitter.instruction("mov rdi, 0"); // abs(x) for x < 0 is 0 - x + } + } + abi::emit_call_label(ctx.emitter, "__rt_int_sub_checked"); + ctx.emitter.label(&done_label); + Ok(PhpType::Mixed) +} + /// Emits absolute value for the loaded integer result. fn emit_int_abs(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { diff --git a/src/codegen/lower_inst/builtins/math/binary.rs b/src/codegen/lower_inst/builtins/math/binary.rs index 57a39edb38..ee7569f55e 100644 --- a/src/codegen/lower_inst/builtins/math/binary.rs +++ b/src/codegen/lower_inst/builtins/math/binary.rs @@ -23,7 +23,7 @@ pub(crate) fn lower_intdiv( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "intdiv", 2)?; + super::super::ensure_arg_count(inst, "intdiv", 2)?; let zero_label = ctx.next_label("intdiv_zero"); let overflow_label = ctx.next_label("intdiv_overflow"); let done_label = ctx.next_label("intdiv_done"); @@ -68,7 +68,7 @@ pub(crate) fn lower_fdiv( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "fdiv", 2)?; + super::super::ensure_arg_count(inst, "fdiv", 2)?; let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; super::load_numeric_as_float(ctx, lhs, "fdiv")?; @@ -89,11 +89,16 @@ pub(crate) fn lower_fdiv( } /// Lowers `fmod()` for concrete integer-like and floating operands. +/// +/// Both targets call libc `fmod`, which is IEEE-754 `remainder`-truncated: the result +/// carries the sign of the *dividend*, so `fmod(-7.5, 2.5)` is `-0.0` and `echo`s as `-0` +/// exactly like PHP. Recomputing it as `x - trunc(x / y) * y` loses that signed zero +/// (the subtraction yields `+0.0`), which is why the AArch64 path is not open-coded. pub(crate) fn lower_fmod( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "fmod", 2)?; + super::super::ensure_arg_count(inst, "fmod", 2)?; let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; super::load_numeric_as_float(ctx, lhs, "fmod")?; @@ -101,10 +106,9 @@ pub(crate) fn lower_fmod( super::load_numeric_as_float(ctx, rhs, "fmod")?; match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_pop_float_reg(ctx.emitter, "d1"); - ctx.emitter.instruction("fdiv d2, d1, d0"); // compute dividend divided by divisor for fmod truncation - ctx.emitter.instruction("frintz d2, d2"); // truncate the quotient toward zero - ctx.emitter.instruction("fmsub d0, d2, d0, d1"); // compute dividend minus truncated quotient times divisor + ctx.emitter.instruction("fmov d1, d0"); // move the divisor into the second libc fmod argument + abi::emit_pop_float_reg(ctx.emitter, "d0"); + ctx.emitter.bl_c("fmod"); } Arch::X86_64 => { abi::emit_pop_float_reg(ctx.emitter, "xmm1"); @@ -122,7 +126,7 @@ pub(crate) fn lower_pow( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "pow", 2)?; + super::super::ensure_arg_count(inst, "pow", 2)?; let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; super::load_numeric_as_float(ctx, lhs, "pow")?; diff --git a/src/codegen/lower_inst/builtins/math/libm.rs b/src/codegen/lower_inst/builtins/math/libm.rs index d180dbddf0..4d49ed13cc 100644 --- a/src/codegen/lower_inst/builtins/math/libm.rs +++ b/src/codegen/lower_inst/builtins/math/libm.rs @@ -24,7 +24,7 @@ pub(crate) fn lower_unary_libm( inst: &Instruction, name: &str, ) -> Result<()> { - super::ensure_arg_count(inst, name, 1)?; + super::super::ensure_arg_count(inst, name, 1)?; let value = expect_operand(inst, 0)?; super::load_numeric_as_float(ctx, value, name)?; ctx.emitter.bl_c(name); @@ -93,7 +93,7 @@ fn lower_binary_libm( inst: &Instruction, name: &str, ) -> Result<()> { - super::ensure_arg_count(inst, name, 2)?; + super::super::ensure_arg_count(inst, name, 2)?; let lhs = expect_operand(inst, 0)?; let rhs = expect_operand(inst, 1)?; super::load_numeric_as_float(ctx, lhs, name)?; @@ -139,7 +139,7 @@ fn lower_angle_conversion( name: &str, factor: f64, ) -> Result<()> { - super::ensure_arg_count(inst, name, 1)?; + super::super::ensure_arg_count(inst, name, 1)?; let value = expect_operand(inst, 0)?; super::load_numeric_as_float(ctx, value, name)?; let label = ctx.data.add_float(factor); diff --git a/src/codegen/lower_inst/builtins/math/min_max_array.rs b/src/codegen/lower_inst/builtins/math/min_max_array.rs new file mode 100644 index 0000000000..52b67d2247 --- /dev/null +++ b/src/codegen/lower_inst/builtins/math/min_max_array.rs @@ -0,0 +1,454 @@ +//! Purpose: +//! Lowers PHP's single-array `min()` / `max()` form for the EIR backend. +//! Reduces an indexed array's payload slots, or a hash-backed table's values, to one +//! element. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::math::lower_min_max()`. +//! +//! Key details: +//! - Indexed arrays store their logical length in the first header word and their +//! payload slots 24 bytes after the header: one 8-byte slot per `int`/`float`/`bool` +//! or boxed-`Mixed` element, one 16-byte `[ptr][len]` slot per string element. +//! - Scalar indexed arrays reduce with an inline loop; string, boxed-`Mixed`, and +//! hash-backed containers reduce through the `__rt_min_max_str` / +//! `__rt_min_max_mixed` / `__rt_min_max_hash` runtime helpers, which apply PHP 8's +//! full comparison table through `__rt_php_compare`. +//! - An empty array is PHP's `ValueError`, thrown through the shared math +//! `emit_throw_value_error()` path so it stays catchable like `clamp()`'s. The +//! runtime reductions report emptiness with runtime tag `-1`. +//! - Scratch is limited to the registers the backend already treats as clobbered by +//! a builtin call (`x9`–`x13`, `d0`/`d1`; `rax`/`rcx`/`rdx`/`r10`/`r11`, `xmm0`/`xmm1`), +//! so no register-allocated value can be destroyed by the loop. + +use crate::codegen::abi; +use crate::codegen::platform::Arch; +use crate::codegen::{CodegenIrError, Result}; +use crate::ir::Instruction; +use crate::types::PhpType; + +use super::super::super::super::context::FunctionContext; +use super::super::expect_operand; + +/// Byte offset of the first payload slot inside an indexed-array allocation. +const ARRAY_DATA_OFFSET: i64 = 24; + +/// Selects the runtime reduction that matches a container's element storage. +#[derive(Clone, Copy)] +enum ContainerReduction { + /// Indexed array of 16-byte `[ptr][len]` string slots (`__rt_min_max_str`). + IndexedStr, + /// Indexed array of borrowed boxed-`Mixed` cells (`__rt_min_max_mixed`). + IndexedMixed, + /// Hash-backed associative array of any value type (`__rt_min_max_hash`). + Hash, +} + +impl ContainerReduction { + /// Returns the `__rt_*` symbol that reduces this container shape. + fn symbol(self) -> &'static str { + match self { + ContainerReduction::IndexedStr => "__rt_min_max_str", + ContainerReduction::IndexedMixed => "__rt_min_max_mixed", + ContainerReduction::Hash => "__rt_min_max_hash", + } + } +} + +/// Lowers PHP's single-argument `min()` / `max()` form and reports whether it applied. +/// +/// Returns `Ok(false)` for the variadic form so the caller keeps its own lowering. +/// Any single-argument call whose operand is an array is handled here — including the +/// element types the reduction cannot compare, which are rejected with an explicit +/// diagnostic rather than falling through to the numeric paths. +pub(super) fn try_lower_single_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + want_max: bool, +) -> Result { + if inst.operands.len() != 1 { + return Ok(false); + } + let array = expect_operand(inst, 0)?; + match ctx.value_php_type(array)?.codegen_repr() { + PhpType::Array(element) => { + let element = element.codegen_repr(); + match element { + PhpType::Str => { + lower_container_min_max(ctx, inst, want_max, ContainerReduction::IndexedStr)? + } + PhpType::Mixed => { + lower_container_min_max(ctx, inst, want_max, ContainerReduction::IndexedMixed)? + } + _ => lower_array_min_max(ctx, inst, want_max, &element)?, + } + Ok(true) + } + PhpType::AssocArray { .. } => { + lower_container_min_max(ctx, inst, want_max, ContainerReduction::Hash)?; + Ok(true) + } + _ => Ok(false), + } +} + +/// Formats the diagnostic for a single-array `min()` / `max()` the reduction cannot compare. +/// +/// Indexed arrays of `int`, `float`, `bool` and `string`, indexed arrays of boxed +/// `Mixed` cells, and hash-backed associative arrays all reduce. What is left are the +/// element representations no reduction can read as a comparable payload: the tagged +/// nullable-scalar slots, whose runtime tag lives in a side register, and homogeneous +/// arrays of a heap shape (`array>`, `array`) that PHP would compare +/// structurally. Rejecting them keeps a wrong ordering out of the generated program. +fn unsupported_element_error(name: &str, shape: &str) -> CodegenIrError { + CodegenIrError::unsupported(format!( + "{}() with a single array argument cannot reduce an array of {} values", + name, shape + )) +} + +/// Lowers `min($array)` / `max($array)` by reducing the array's payload slots. +/// +/// `element` is the array's codegen element representation. Integer-like elements +/// are compared as signed 64-bit words and floating elements through the same +/// `fmin`/`fmax` (AArch64) and `minsd`/`maxsd` (x86_64) selection the variadic form +/// uses, so both call forms agree on every target. An empty array throws PHP's +/// `ValueError` and never falls through to the reduction. +fn lower_array_min_max( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + want_max: bool, + element: &PhpType, +) -> Result<()> { + let name = super::min_max_name(want_max); + let array = expect_operand(inst, 0)?; + let float_elements = match element { + PhpType::Float => true, + // `Void`/`Never` is the element type of an empty array literal and of an + // all-null array: both store zeroed scalar slots, so the integer reduction + // handles them and an empty one reaches the ValueError path. + PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never => false, + other => { + return Err(unsupported_element_error( + name, + &format!("array<{}>", other), + )) + } + }; + let result_ty = inst + .result + .map(|value| ctx.value_php_type(value)) + .transpose()? + .unwrap_or_else(|| element.clone()) + .codegen_repr(); + let empty_label = ctx.next_label("min_max_array_empty"); + let loop_label = ctx.next_label("min_max_array_loop"); + let reduced_label = ctx.next_label("min_max_array_reduced"); + let done_label = ctx.next_label("min_max_array_done"); + let message = format!( + "{}(): Argument #1 ($value) must contain at least one element", + name + ); + let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); + + ctx.load_value_to_result(array)?; + match (ctx.emitter.target.arch, float_elements) { + (Arch::AArch64, false) => { + emit_int_reduce_aarch64(ctx, want_max, &empty_label, &loop_label, &reduced_label) + } + (Arch::AArch64, true) => { + emit_float_reduce_aarch64(ctx, want_max, &empty_label, &loop_label, &reduced_label) + } + (Arch::X86_64, false) => { + emit_int_reduce_x86_64(ctx, want_max, &empty_label, &loop_label, &reduced_label) + } + (Arch::X86_64, true) => { + emit_float_reduce_x86_64(ctx, want_max, &empty_label, &loop_label, &reduced_label) + } + } + ctx.emitter.label(&reduced_label); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&empty_label); + super::emit_throw_value_error(ctx, &message_label, message_len); + ctx.emitter.label(&done_label); + materialize_result(ctx, element, float_elements, &result_ty) +} + +/// Converts the reduced element into the representation the EIR result value expects. +/// +/// EIR array element types and checker call-site types are inferred separately, so a +/// reduction over an `array` can still feed a boxed `Mixed` result. The reduced +/// element is boxed with its own element type (so `min([true, false])` stays `bool`) +/// and int-typed elements promote when the result value is a float. +fn materialize_result( + ctx: &mut FunctionContext<'_>, + element: &PhpType, + float_elements: bool, + result_ty: &PhpType, +) -> Result<()> { + match result_ty { + PhpType::Mixed | PhpType::Union(_) => { + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, element); + Ok(()) + } + PhpType::Float if !float_elements => { + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + _ => Ok(()), + } +} + +/// Lowers `min($container)` / `max($container)` through a runtime reduction helper. +/// +/// Handles the container shapes whose elements are not raw 8-byte scalar words: +/// indexed `array`, indexed arrays of boxed `Mixed` cells, and hash-backed +/// associative arrays. The helper returns the winning element as an unboxed +/// `(tag, lo, hi)` triple, or tag `-1` for an empty container, which is turned into +/// PHP's catchable `ValueError` here. +fn lower_container_min_max( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + want_max: bool, + reduction: ContainerReduction, +) -> Result<()> { + let name = super::min_max_name(want_max); + let array = expect_operand(inst, 0)?; + let result_ty = inst + .result + .map(|value| ctx.value_php_type(value)) + .transpose()? + .unwrap_or(PhpType::Mixed) + .codegen_repr(); + let empty_label = ctx.next_label("min_max_container_empty"); + let done_label = ctx.next_label("min_max_container_done"); + let message = format!( + "{}(): Argument #1 ($value) must contain at least one element", + name + ); + let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); + let direction = i64::from(want_max); + + ctx.load_value_to_result(array)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("mov x1, #{}", direction)); // pass 1 for max() and 0 for min() as the reduction direction + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // move the container pointer into the reduction argument register + ctx.emitter.instruction(&format!("mov rsi, {}", direction)); // pass 1 for max() and 0 for min() as the reduction direction + } + } + abi::emit_call_label(ctx.emitter, reduction.symbol()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmn x0, #1"); // did the reduction report the empty-container tag? + ctx.emitter.instruction(&format!("b.eq {}", empty_label)); // an empty container is PHP's ValueError, not a reduction + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, -1"); // did the reduction report the empty-container tag? + ctx.emitter.instruction(&format!("je {}", empty_label)); // an empty container is PHP's ValueError, not a reduction + } + } + materialize_container_result(ctx, &result_ty, name)?; + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&empty_label); + super::emit_throw_value_error(ctx, &message_label, message_len); + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Converts a reduced `(tag, lo, hi)` triple into the EIR result value's representation. +/// +/// The triple already sits in the registers `__rt_mixed_from_value` consumes and in the +/// registers a string result is returned in on AArch64, so the boxed and string cases +/// cost at most a register move. Numeric results carry a defensive tag check so an +/// element whose runtime tag disagrees with the inferred result type is converted +/// instead of reinterpreted. +fn materialize_container_result( + ctx: &mut FunctionContext<'_>, + result_ty: &PhpType, + name: &str, +) -> Result<()> { + match result_ty { + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + Ok(()) + } + PhpType::Str => { + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rax, rdi"); // publish the reduced string pointer in the string result register + ctx.emitter.instruction("mov rdx, rsi"); // publish the reduced string length in the string result register + } + // The reduction borrows the winning bytes from the container, which the + // caller is free to release right after this call, so the result has to + // own its own copy. + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + Ok(()) + } + PhpType::Float => { + let double_label = ctx.next_label("min_max_container_double"); + let ready_label = ctx.next_label("min_max_container_float_ready"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #2"); // is the reduced element already a float payload? + ctx.emitter.instruction(&format!("b.eq {}", double_label)); // reinterpret its payload word directly + ctx.emitter.instruction("scvtf d0, x1"); // widen an integer-like payload into the float result register + abi::emit_jump(ctx.emitter, &ready_label); + ctx.emitter.label(&double_label); + ctx.emitter.instruction("fmov d0, x1"); // reinterpret the payload word as the double it encodes + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 2"); // is the reduced element already a float payload? + ctx.emitter.instruction(&format!("je {}", double_label)); // reinterpret its payload word directly + ctx.emitter.instruction("cvtsi2sd xmm0, rdi"); // widen an integer-like payload into the float result register + abi::emit_jump(ctx.emitter, &ready_label); + ctx.emitter.label(&double_label); + ctx.emitter.instruction("movq xmm0, rdi"); // reinterpret the payload word as the double it encodes + } + } + ctx.emitter.label(&ready_label); + Ok(()) + } + PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never => { + let ready_label = ctx.next_label("min_max_container_int_ready"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #2"); // is the reduced element a float payload? + ctx.emitter.instruction(&format!("b.ne {}", ready_label)); // integer-like payloads publish unchanged + ctx.emitter.instruction("fmov d0, x1"); // reinterpret the payload word as the double it encodes + ctx.emitter.instruction("fcvtzs x1, d0"); // truncate the double toward zero like PHP's int cast + ctx.emitter.label(&ready_label); + ctx.emitter.instruction("mov x0, x1"); // publish the reduced payload in the integer result register + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 2"); // is the reduced element a float payload? + ctx.emitter.instruction(&format!("jne {}", ready_label)); // integer-like payloads publish unchanged + ctx.emitter.instruction("movq xmm0, rdi"); // reinterpret the payload word as the double it encodes + ctx.emitter.instruction("cvttsd2si rdi, xmm0"); // truncate the double toward zero like PHP's int cast + ctx.emitter.label(&ready_label); + ctx.emitter.instruction("mov rax, rdi"); // publish the reduced payload in the integer result register + } + } + Ok(()) + } + other => Err(unsupported_element_error(name, &format!("{}", other))), + } +} + +/// Emits the AArch64 integer reduction over an indexed array's payload slots. +fn emit_int_reduce_aarch64( + ctx: &mut FunctionContext<'_>, + want_max: bool, + empty_label: &str, + loop_label: &str, + reduced_label: &str, +) { + let exit_label = ctx.next_label("min_max_array_exit"); + let condition = if want_max { "gt" } else { "lt" }; + // -- seed the reduction with the first payload slot -- + ctx.emitter.instruction("ldr x9, [x0]"); // x9 = the array's logical element count from its header + ctx.emitter.instruction(&format!("cbz x9, {}", empty_label)); // an empty array is PHP's ValueError, not a reduction + ctx.emitter.instruction(&format!("add x10, x0, #{}", ARRAY_DATA_OFFSET)); // x10 = address of the first payload slot + ctx.emitter.instruction("ldr x11, [x10]"); // x11 = running result seeded with element 0 + ctx.emitter.instruction("mov x12, #1"); // x12 = cursor starting at the second element + // -- fold every remaining element into the running result -- + ctx.emitter.label(loop_label); + ctx.emitter.instruction("cmp x12, x9"); // compare the cursor against the element count + ctx.emitter.instruction(&format!("b.ge {}", exit_label)); // stop once every element has been folded in + ctx.emitter.instruction("ldr x13, [x10, x12, lsl #3]"); // x13 = the payload slot the cursor points at + ctx.emitter.instruction("cmp x13, x11"); // compare the candidate against the running result + ctx.emitter.instruction(&format!("csel x11, x13, x11, {}", condition)); // keep whichever element wins the min/max comparison + ctx.emitter.instruction("add x12, x12, #1"); // advance the cursor to the next payload slot + abi::emit_jump(ctx.emitter, loop_label); + ctx.emitter.label(&exit_label); + ctx.emitter.instruction("mov x0, x11"); // publish the reduced element in the integer result register + abi::emit_jump(ctx.emitter, reduced_label); +} + +/// Emits the AArch64 floating reduction over an indexed array's payload slots. +fn emit_float_reduce_aarch64( + ctx: &mut FunctionContext<'_>, + want_max: bool, + empty_label: &str, + loop_label: &str, + reduced_label: &str, +) { + let exit_label = ctx.next_label("min_max_array_exit"); + let select = if want_max { "fmax" } else { "fmin" }; + // -- seed the reduction with the first payload slot -- + ctx.emitter.instruction("ldr x9, [x0]"); // x9 = the array's logical element count from its header + ctx.emitter.instruction(&format!("cbz x9, {}", empty_label)); // an empty array is PHP's ValueError, not a reduction + ctx.emitter.instruction(&format!("add x10, x0, #{}", ARRAY_DATA_OFFSET)); // x10 = address of the first payload slot + ctx.emitter.instruction("ldr d0, [x10]"); // d0 = running result seeded with element 0 + ctx.emitter.instruction("mov x12, #1"); // x12 = cursor starting at the second element + // -- fold every remaining element into the running result -- + ctx.emitter.label(loop_label); + ctx.emitter.instruction("cmp x12, x9"); // compare the cursor against the element count + ctx.emitter.instruction(&format!("b.ge {}", exit_label)); // stop once every element has been folded in + ctx.emitter.instruction("ldr d1, [x10, x12, lsl #3]"); // d1 = the payload slot the cursor points at + ctx.emitter.instruction(&format!("{} d0, d1, d0", select)); // keep whichever element wins the min/max comparison + ctx.emitter.instruction("add x12, x12, #1"); // advance the cursor to the next payload slot + abi::emit_jump(ctx.emitter, loop_label); + ctx.emitter.label(&exit_label); + abi::emit_jump(ctx.emitter, reduced_label); +} + +/// Emits the x86_64 integer reduction over an indexed array's payload slots. +fn emit_int_reduce_x86_64( + ctx: &mut FunctionContext<'_>, + want_max: bool, + empty_label: &str, + loop_label: &str, + reduced_label: &str, +) { + let exit_label = ctx.next_label("min_max_array_exit"); + let select = if want_max { "cmovg" } else { "cmovl" }; + // -- seed the reduction with the first payload slot -- + ctx.emitter.instruction("mov r10, QWORD PTR [rax]"); // r10 = the array's logical element count from its header + ctx.emitter.instruction("test r10, r10"); // check whether the array holds any element at all + ctx.emitter.instruction(&format!("jz {}", empty_label)); // an empty array is PHP's ValueError, not a reduction + ctx.emitter.instruction(&format!("lea r11, [rax + {}]", ARRAY_DATA_OFFSET)); // r11 = address of the first payload slot + ctx.emitter.instruction("mov rax, QWORD PTR [r11]"); // rax = running result seeded with element 0 + ctx.emitter.instruction("mov rcx, 1"); // rcx = cursor starting at the second element + // -- fold every remaining element into the running result -- + ctx.emitter.label(loop_label); + ctx.emitter.instruction("cmp rcx, r10"); // compare the cursor against the element count + ctx.emitter.instruction(&format!("jge {}", exit_label)); // stop once every element has been folded in + ctx.emitter.instruction("mov rdx, QWORD PTR [r11 + rcx * 8]"); // rdx = the payload slot the cursor points at + ctx.emitter.instruction("cmp rdx, rax"); // compare the candidate against the running result + ctx.emitter.instruction(&format!("{} rax, rdx", select)); // keep whichever element wins the min/max comparison + ctx.emitter.instruction("add rcx, 1"); // advance the cursor to the next payload slot + abi::emit_jump(ctx.emitter, loop_label); + ctx.emitter.label(&exit_label); + abi::emit_jump(ctx.emitter, reduced_label); +} + +/// Emits the x86_64 floating reduction over an indexed array's payload slots. +fn emit_float_reduce_x86_64( + ctx: &mut FunctionContext<'_>, + want_max: bool, + empty_label: &str, + loop_label: &str, + reduced_label: &str, +) { + let exit_label = ctx.next_label("min_max_array_exit"); + let select = if want_max { "maxsd" } else { "minsd" }; + // -- seed the reduction with the first payload slot -- + ctx.emitter.instruction("mov r10, QWORD PTR [rax]"); // r10 = the array's logical element count from its header + ctx.emitter.instruction("test r10, r10"); // check whether the array holds any element at all + ctx.emitter.instruction(&format!("jz {}", empty_label)); // an empty array is PHP's ValueError, not a reduction + ctx.emitter.instruction(&format!("lea r11, [rax + {}]", ARRAY_DATA_OFFSET)); // r11 = address of the first payload slot + ctx.emitter.instruction("movsd xmm0, QWORD PTR [r11]"); // xmm0 = running result seeded with element 0 + ctx.emitter.instruction("mov rcx, 1"); // rcx = cursor starting at the second element + // -- fold every remaining element into the running result -- + ctx.emitter.label(loop_label); + ctx.emitter.instruction("cmp rcx, r10"); // compare the cursor against the element count + ctx.emitter.instruction(&format!("jge {}", exit_label)); // stop once every element has been folded in + ctx.emitter.instruction("movsd xmm1, QWORD PTR [r11 + rcx * 8]"); // xmm1 = the payload slot the cursor points at + ctx.emitter.instruction(&format!("{} xmm0, xmm1", select)); // keep whichever element wins the min/max comparison + ctx.emitter.instruction("add rcx, 1"); // advance the cursor to the next payload slot + abi::emit_jump(ctx.emitter, loop_label); + ctx.emitter.label(&exit_label); + abi::emit_jump(ctx.emitter, reduced_label); +} diff --git a/src/codegen/lower_inst/builtins/math/random.rs b/src/codegen/lower_inst/builtins/math/random.rs index 4f21d8b516..54d7919ca1 100644 --- a/src/codegen/lower_inst/builtins/math/random.rs +++ b/src/codegen/lower_inst/builtins/math/random.rs @@ -7,6 +7,10 @@ //! Key details: //! - Range arguments are evaluated by AST-to-EIR in PHP source order; this module //! reloads the SSA slots and preserves the lower bound across runtime helper calls. +//! - The three range builtins disagree about an inverted range, and each follows php-src: +//! `random_int()` and `mt_rand()` raise a catchable `ValueError` with their own wording, +//! while `rand()` silently swaps the bounds. Without the guard the width `max - min + 1` +//! went negative and `__rt_random_uniform` returned an unbounded garbage integer. use crate::codegen::abi; use crate::codegen::platform::Arch; @@ -17,15 +21,37 @@ use crate::types::PhpType; use super::super::super::super::context::FunctionContext; use super::super::{expect_operand, store_if_result}; +/// What a random-range builtin does when `$min` turns out to be greater than `$max`. +#[derive(Clone, Copy)] +enum InvertedRangePolicy { + /// `rand()` silently samples the swapped `[max, min]` range, exactly like php-src. + Swap, + /// `random_int()` and `mt_rand()` raise a catchable `ValueError` carrying this message. + Throw(&'static str), +} + +/// php-src's verbatim `ValueError` wording for `random_int()` with `$min > $max`. +const RANDOM_INT_INVERTED_RANGE_MESSAGE: &str = + "random_int(): Argument #1 ($min) must be less than or equal to argument #2 ($max)"; + +/// php-src's verbatim `ValueError` wording for `mt_rand()` with `$min > $max`. +const MT_RAND_INVERTED_RANGE_MESSAGE: &str = + "mt_rand(): Argument #2 ($max) must be greater than or equal to argument #1 ($min)"; + /// Lowers `rand()` and `mt_rand()` with either zero args or an inclusive range. pub(crate) fn lower_rand( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, ) -> Result<()> { + let policy = if name == "mt_rand" { + InvertedRangePolicy::Throw(MT_RAND_INVERTED_RANGE_MESSAGE) + } else { + InvertedRangePolicy::Swap + }; match inst.operands.len() { 0 => abi::emit_call_label(ctx.emitter, "__rt_random_u32"), - 2 => lower_random_range(ctx, inst, name)?, + 2 => lower_random_range(ctx, inst, name, policy)?, count => { return Err(CodegenIrError::invalid_module(format!( "{} expected 0 or 2 args, got {}", @@ -41,8 +67,13 @@ pub(crate) fn lower_random_int( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "random_int", 2)?; - lower_random_range(ctx, inst, "random_int")?; + super::super::ensure_arg_count(inst, "random_int", 2)?; + lower_random_range( + ctx, + inst, + "random_int", + InvertedRangePolicy::Throw(RANDOM_INT_INVERTED_RANGE_MESSAGE), + )?; store_if_result(ctx, inst) } @@ -51,6 +82,7 @@ fn lower_random_range( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, + policy: InvertedRangePolicy, ) -> Result<()> { let min = expect_operand(inst, 0)?; let max = expect_operand(inst, 1)?; @@ -58,14 +90,18 @@ fn lower_random_range( abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); load_numeric_as_int(ctx, max, name)?; match ctx.emitter.target.arch { - Arch::AArch64 => emit_aarch64_random_range(ctx), - Arch::X86_64 => emit_x86_64_random_range(ctx), + Arch::AArch64 => emit_aarch64_random_range(ctx, policy), + Arch::X86_64 => emit_x86_64_random_range(ctx, policy), } } /// Emits the AArch64 range normalization and runtime call. -fn emit_aarch64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { +fn emit_aarch64_random_range( + ctx: &mut FunctionContext<'_>, + policy: InvertedRangePolicy, +) -> Result<()> { abi::emit_pop_reg(ctx.emitter, "x9"); + emit_inverted_range_policy(ctx, policy, "x9", "x0"); ctx.emitter.instruction("sub x0, x0, x9"); // compute the inclusive range width as max - min ctx.emitter.instruction("add x0, x0, #1"); // convert the width to the exclusive upper bound for the random helper abi::emit_push_reg(ctx.emitter, "x9"); @@ -76,8 +112,12 @@ fn emit_aarch64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { } /// Emits the x86_64 range normalization and runtime call. -fn emit_x86_64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { +fn emit_x86_64_random_range( + ctx: &mut FunctionContext<'_>, + policy: InvertedRangePolicy, +) -> Result<()> { abi::emit_pop_reg(ctx.emitter, "r9"); + emit_inverted_range_policy(ctx, policy, "r9", "rax"); ctx.emitter.instruction("sub rax, r9"); // compute the inclusive range width as max - min ctx.emitter.instruction("add rax, 1"); // convert the width to the exclusive upper bound for the random helper ctx.emitter.instruction("mov rdi, rax"); // pass the exclusive upper bound to the random helper @@ -86,6 +126,56 @@ fn emit_x86_64_random_range(ctx: &mut FunctionContext<'_>) -> Result<()> { Ok(()) } +/// Normalizes or rejects an inverted `[min, max]` range before the width is computed. +/// +/// `min_reg` and `max_reg` still hold the materialized bounds, so a swap is a plain register +/// exchange and a rejection is a compare plus the shared `ValueError` sequence. Letting an +/// inverted range through would make `max - min + 1` non-positive, and `__rt_random_uniform` +/// treats that as an unbounded modulus, which is where the garbage `random_int(10, 5)` value +/// came from. +fn emit_inverted_range_policy( + ctx: &mut FunctionContext<'_>, + policy: InvertedRangePolicy, + min_reg: &str, + max_reg: &str, +) { + match policy { + InvertedRangePolicy::Throw(message) => { + let ok_label = ctx.next_label("random_range_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", min_reg, max_reg)); // is the requested range inverted? + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // an ordered range samples normally + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", min_reg, max_reg)); // is the requested range inverted? + ctx.emitter.instruction(&format!("jle {}", ok_label)); // an ordered range samples normally + } + } + crate::codegen::lower_inst::exceptions::emit_value_error(ctx, message); + ctx.emitter.label(&ok_label); + } + InvertedRangePolicy::Swap => { + let ok_label = ctx.next_label("random_range_ordered"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", min_reg, max_reg)); // is the requested range inverted? + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // an ordered range needs no swap + ctx.emitter.instruction(&format!("mov x10, {}", min_reg)); // park the larger bound while the pair is exchanged + ctx.emitter.instruction(&format!("mov {}, {}", min_reg, max_reg)); // the smaller bound becomes the range minimum + ctx.emitter.instruction(&format!("mov {}, x10", max_reg)); // the larger bound becomes the range maximum + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", min_reg, max_reg)); // is the requested range inverted? + ctx.emitter.instruction(&format!("jle {}", ok_label)); // an ordered range needs no swap + ctx.emitter.instruction(&format!("xchg {}, {}", min_reg, max_reg)); // exchange the inverted bounds so the width stays positive + } + } + ctx.emitter.label(&ok_label); + } + } +} + /// Loads a numeric range operand and normalizes values into the integer result register. fn load_numeric_as_int( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/object_props.rs b/src/codegen/lower_inst/builtins/object_props.rs new file mode 100644 index 0000000000..6f899ba31c --- /dev/null +++ b/src/codegen/lower_inst/builtins/object_props.rs @@ -0,0 +1,205 @@ +//! Purpose: +//! Lowers the four internal `__elephc_object_*` introspection builtins the injected +//! `var_export` prelude uses to walk an object: `__elephc_object_is_enum`, +//! `__elephc_object_prop_count`, `__elephc_object_prop_name` and +//! `__elephc_object_prop_value`. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::runtime_functions` dispatch, through +//! the `RuntimeFnId::ElephcObject*` targets declared by the builtin registry. +//! +//! Key details: +//! - Every one of them starts by materializing a RAW OBJECT POINTER (or 0) in the +//! integer result register, so the runtime helpers never have to know whether the +//! caller had a statically typed object or a boxed `Mixed`. `Mixed` operands are +//! unboxed here and a non-object payload collapses to 0, which each helper +//! already treats as "no object". +//! - The helpers themselves live in +//! `codegen_support::runtime::objects::{enum_debug, export_props}` and read the +//! same `_class_prop_desc_*` rows `print_r` and `var_dump` walk. +//! - There is no target-specific behavior beyond register naming; both supported +//! architectures go through the same sequence. + +use crate::codegen::abi; +use crate::codegen::platform::Arch; +use crate::codegen::Result; +use crate::ir::{Instruction, ValueId}; +use crate::types::PhpType; + +use super::super::super::context::FunctionContext; +use super::{expect_operand, store_if_result}; + +/// Lowers `__elephc_object_is_enum(value)` to a bounded per-class table probe. +/// +/// Returns PHP `true` only for an instance whose class is an enum; a non-object +/// value, a null instance and an unknown class id all report `false`. +pub(crate) fn lower_object_is_enum( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "__elephc_object_is_enum", 1)?; + ctx.emitter.blank(); + ctx.emitter.comment("__elephc_object_is_enum()"); + emit_object_pointer_from_operand(ctx, expect_operand(inst, 0)?)?; + let skip_label = ctx.next_label("obj_is_enum_not_object"); + let done_label = ctx.next_label("obj_is_enum_done"); + emit_branch_if_result_zero(ctx, &skip_label); + abi::emit_call_label(ctx.emitter, "__rt_obj_enum_kind"); + // The kind is 0 for a plain class and 1/2/3 for a pure / int-backed / + // string-backed enum, so PHP's boolean is "kind is non-zero". + emit_normalize_result_to_bool(ctx); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&skip_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); + store_if_result(ctx, inst) +} + +/// Lowers `__elephc_object_prop_count(value)` to `__rt_obj_prop_count`. +/// +/// A non-object operand reaches the helper as a null pointer, which reports 0. +pub(crate) fn lower_object_prop_count( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "__elephc_object_prop_count", 1)?; + ctx.emitter.blank(); + ctx.emitter.comment("__elephc_object_prop_count()"); + emit_object_pointer_from_operand(ctx, expect_operand(inst, 0)?)?; + abi::emit_call_label(ctx.emitter, "__rt_obj_prop_count"); + store_if_result(ctx, inst) +} + +/// Lowers `__elephc_object_prop_name(value, index)` to `__rt_obj_prop_name`. +/// +/// The result is the platform string result pair; an absent or uninitialized +/// property yields a zero-length string. +pub(crate) fn lower_object_prop_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "__elephc_object_prop_name", 2)?; + ctx.emitter.blank(); + ctx.emitter.comment("__elephc_object_prop_name()"); + emit_object_and_index_arguments(ctx, inst)?; + abi::emit_call_label(ctx.emitter, "__rt_obj_prop_name"); + store_if_result(ctx, inst) +} + +/// Lowers `__elephc_object_prop_value(value, index)` to `__rt_obj_prop_value`. +/// +/// The helper always returns a freshly allocated Mixed cell (boxed PHP null when +/// there is no such property), matching the `Fresh` ownership the registry +/// declares for this target. +pub(crate) fn lower_object_prop_value( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "__elephc_object_prop_value", 2)?; + ctx.emitter.blank(); + ctx.emitter.comment("__elephc_object_prop_value()"); + emit_object_and_index_arguments(ctx, inst)?; + abi::emit_call_label(ctx.emitter, "__rt_obj_prop_value"); + store_if_result(ctx, inst) +} + +/// Materializes the object pointer in the first argument register and the property +/// index in the second, evaluating the index FIRST so the object pointer is not +/// clobbered by the index load. +fn emit_object_and_index_arguments( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let index = expect_operand(inst, 1)?; + let index_reg = abi::secondary_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(index, index_reg)?; + abi::emit_push_reg(ctx.emitter, index_reg); + emit_object_pointer_from_operand(ctx, expect_operand(inst, 0)?)?; + abi::emit_pop_reg(ctx.emitter, index_reg); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("mov x1, {}", index_reg)); // property index → second helper argument + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // object pointer → SysV first argument register + ctx.emitter + .instruction(&format!("mov rsi, {}", index_reg)); // property index → SysV second argument register + } + } + Ok(()) +} + +/// Leaves a raw object pointer (or 0) in the integer result register. +/// +/// A statically typed object operand is already a pointer. A `Mixed` operand is +/// unboxed and only a tag-6 payload survives; every other shape — including PHP +/// null — collapses to 0, which the runtime helpers read as "no object". +fn emit_object_pointer_from_operand( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + let loaded = ctx.load_value_to_result(value)?.codegen_repr(); + match loaded { + PhpType::Object(_) => Ok(()), + PhpType::Mixed | PhpType::Union(_) => { + let not_object = ctx.next_label("obj_props_not_object"); + let done = ctx.next_label("obj_props_unboxed"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // only a boxed object payload carries properties + ctx.emitter + .instruction(&format!("b.ne {}", not_object)); // every other Mixed shape reports "no object" + ctx.emitter.instruction("mov x0, x1"); // unboxed object pointer → integer result register + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // only a boxed object payload carries properties + ctx.emitter + .instruction(&format!("jne {}", not_object)); // every other Mixed shape reports "no object" + ctx.emitter.instruction("mov rax, rdi"); // unboxed object pointer → integer result register + } + } + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(¬_object); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done); + Ok(()) + } + _ => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + Ok(()) + } + } +} + +/// Branches to `label` when the integer result register holds zero. +fn emit_branch_if_result_zero(ctx: &mut FunctionContext<'_>, label: &str) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cbz {}, {}", result_reg, label)); // a null object pointer takes the caller's empty path + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); // is the object pointer zero? + ctx.emitter.instruction(&format!("jz {}", label)); // a null object pointer takes the caller's empty path + } + } +} + +/// Collapses a non-zero integer result to PHP `true` (1) and zero to `false` (0). +fn emit_normalize_result_to_bool(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #0"); // compare the enum kind against zero + ctx.emitter.instruction("cset x0, ne"); // any non-zero enum kind is PHP true + } + Arch::X86_64 => { + ctx.emitter.instruction("test rax, rax"); // compare the enum kind against zero + ctx.emitter.instruction("setne al"); // any non-zero enum kind is PHP true + ctx.emitter.instruction("movzx rax, al"); // widen the boolean to the full result register + } + } +} diff --git a/src/codegen/lower_inst/builtins/round_mode.rs b/src/codegen/lower_inst/builtins/round_mode.rs new file mode 100644 index 0000000000..c93db6ec7a --- /dev/null +++ b/src/codegen/lower_inst/builtins/round_mode.rs @@ -0,0 +1,168 @@ +//! Purpose: +//! Lowers `round($num, $precision, $mode)` — the precision-carrying forms of PHP's `round()` — +//! onto the shared `__rt_round_mode` runtime routine. +//! +//! Called from: +//! - `crate::codegen::lower_inst::runtime_functions::group_07` for `RuntimeFnId::Round`. +//! +//! Key details: +//! - The single-argument form still lowers through +//! `crate::codegen::lower_inst::builtins::math::lower_round()`, which emits the target's +//! native ties-away-from-zero instruction. That is exactly `PHP_ROUND_HALF_UP` at precision +//! zero, so both paths agree. +//! - Two- and three-argument calls go through `__rt_round_mode`, a port of php-src 8.4's +//! `_php_math_round()`. The runtime routine — not this lowering — owns the tie-breaking and +//! the integral-part correction; this file only materializes the ABI and the argument guard. +//! - `$mode` is validated HERE rather than in the runtime routine so the failure raises PHP's +//! catchable `ValueError` through the ordinary codegen exception path, exactly like +//! `str_pad()`'s `$pad_type` guard. + +use crate::codegen::abi; +use crate::codegen::platform::Arch; +use crate::ir::{Instruction, ValueId}; +use crate::types::PhpType; + +use crate::codegen::{CodegenIrError, Result}; + +use super::super::super::context::FunctionContext; +use super::super::load_value_to_first_int_arg; +use super::{ensure_arg_count_between, expect_operand, store_if_result}; + +/// php-src's verbatim `ValueError` wording for an unknown `round()` rounding mode. +const ROUND_MODE_MESSAGE: &str = + "round(): Argument #3 ($mode) must be a valid rounding mode (RoundingMode::*)"; + +/// The lowest php-src rounding-mode integer (`PHP_ROUND_HALF_UP`). +const ROUND_MODE_MIN: i64 = 1; + +/// The highest php-src rounding-mode integer (`RoundingMode::AwayFromZero`). +const ROUND_MODE_MAX: i64 = 8; + +/// Lowers every arity of PHP's `round()`. +/// +/// One operand keeps the native single-instruction path; two or three operands materialize +/// `(value, precision, mode)` for `__rt_round_mode` and raise `ValueError` for a `$mode` +/// outside php-src's `1..=8` enumeration. +pub(crate) fn lower_round(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + ensure_arg_count_between(inst, "round", 1, 3)?; + if inst.operands.len() == 1 { + return super::math::lower_round(ctx, inst); + } + let value = expect_operand(inst, 0)?; + let precision = expect_operand(inst, 1)?; + let mode = if inst.operands.len() == 3 { + Some(expect_operand(inst, 2)?) + } else { + None + }; + + // PHP evaluates every argument before validating `$mode`, and the operands are already + // lowered SSA values here, so the guard can sit next to the ABI materialization. + load_precision_as_int(ctx, precision)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_mode_operand(ctx, mode)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + load_numeric_as_float(ctx, value)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x1"); + abi::emit_pop_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "rsi"); + abi::emit_pop_reg(ctx.emitter, "rdi"); + } + } + abi::emit_call_label(ctx.emitter, "__rt_round_mode"); + store_if_result(ctx, inst) +} + +/// Materializes `$mode` in the integer result register and rejects values PHP refuses. +/// +/// An omitted `$mode` is `PHP_ROUND_HALF_UP`, so a two-argument call materializes the literal +/// `1` and needs no guard. A supplied mode is range-checked against php-src's `1..=8` +/// enumeration while it still sits in the integer result register. +fn emit_mode_operand(ctx: &mut FunctionContext<'_>, mode: Option) -> Result<()> { + let Some(mode) = mode else { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + ROUND_MODE_MIN, + ); + return Ok(()); + }; + load_precision_as_int(ctx, mode)?; + let mode_reg = abi::int_result_reg(ctx.emitter); + crate::codegen::lower_inst::exceptions::emit_value_error_unless( + ctx, + crate::codegen::lower_inst::exceptions::ValueGuard::SignedInRange( + mode_reg, + ROUND_MODE_MIN, + ROUND_MODE_MAX, + ), + ROUND_MODE_MESSAGE, + ); + Ok(()) +} + +/// Loads a `round()` integer operand (`$precision` or `$mode`) into the integer result register. +/// +/// Mirrors PHP's `int` parameter coercion for the representations the backend can carry: +/// integers and booleans are already integral, `null` coerces to `0`, and a float or boxed +/// `Mixed` goes through the shared PHP float→int conversion. +fn load_precision_as_int(ctx: &mut FunctionContext<'_>, value: ValueId) -> Result<()> { + match ctx.load_value_to_result(value)?.codegen_repr() { + PhpType::Int | PhpType::Bool => Ok(()), + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + Ok(()) + } + PhpType::Float => { + abi::emit_float_result_to_int_result(ctx.emitter); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "round integer argument for PHP type {:?}", + other + ))), + } +} + +/// Loads `round()`'s `$num` operand into the floating-point result register. +/// +/// Reproduces `math::load_numeric_as_float()` for the operand shapes `round()` accepts: +/// concrete floats pass through, integers and booleans convert, `null` becomes `0.0`, and +/// boxed `Mixed` goes through the shared runtime float coercion. +fn load_numeric_as_float(ctx: &mut FunctionContext<'_>, value: ValueId) -> Result<()> { + match ctx.load_value_to_result(value)?.codegen_repr() { + PhpType::Float => Ok(()), + PhpType::Int | PhpType::Bool => { + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + PhpType::TaggedScalar => { + crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "round for PHP type {:?}", + other + ))), + } +} diff --git a/src/codegen/lower_inst/builtins/spl.rs b/src/codegen/lower_inst/builtins/spl.rs index a9cc2db8af..cf7f9e33ab 100644 --- a/src/codegen/lower_inst/builtins/spl.rs +++ b/src/codegen/lower_inst/builtins/spl.rs @@ -16,7 +16,7 @@ use crate::codegen::{ use crate::codegen::platform::Arch; use crate::codegen::{CodegenIrError, Result}; use crate::ir::{BlockId, Immediate, Instruction, Op, ValueDef, ValueId}; -use crate::names::function_symbol; +use crate::names::{function_symbol, label_fragment}; use crate::types::PhpType; use super::super::super::context::FunctionContext; @@ -1450,11 +1450,11 @@ fn emit_integer_key_from_result(ctx: &mut FunctionContext<'_>) { fn emit_float_key_from_result(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float iterator keys to integer array keys + abi::emit_php_float_to_int(ctx.emitter, "x1"); ctx.emitter.instruction("mov x2, #-1"); // key_hi sentinel marks an integer key } Arch::X86_64 => { - ctx.emitter.instruction("cvttsd2si rax, xmm0"); // PHP casts float iterator keys to integer array keys + abi::emit_php_float_to_int(ctx.emitter, "rax"); ctx.emitter.instruction("mov rdx, -1"); // key_hi sentinel marks an integer key } } @@ -1499,7 +1499,7 @@ fn emit_mixed_key_from_result(ctx: &mut FunctionContext<'_>) -> Result<()> { ctx.emitter.label(&float_label); ctx.emitter.instruction("fmov d0, x1"); // reinterpret the unboxed float payload bits for casting - ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "x1"); ctx.emitter.instruction("mov x2, #-1"); // mark the converted float payload as an integer key ctx.emitter.instruction(&format!("b {}", done_label)); // finish normalized mixed-key handling @@ -1539,7 +1539,7 @@ fn emit_mixed_key_from_result(ctx: &mut FunctionContext<'_>) -> Result<()> { ctx.emitter.label(&float_label); ctx.emitter.instruction("movq xmm0, rdi"); // reinterpret the unboxed float payload bits for casting - ctx.emitter.instruction("cvttsd2si rax, xmm0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "rax"); ctx.emitter.instruction("mov rdx, -1"); // mark the converted float payload as an integer key ctx.emitter.instruction(&format!("jmp {}", done_label)); // finish normalized mixed-key handling @@ -1772,13 +1772,6 @@ fn emit_branch_if_saved_apply_callback_name_matches( } } -/// Converts PHP function names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits a case-insensitive compare against the saved `iterator_apply()` callback name. fn emit_apply_callback_name_compare( diff --git a/src/codegen/lower_inst/builtins/strings.rs b/src/codegen/lower_inst/builtins/strings.rs index a5ee60f89a..addd4ab12f 100644 --- a/src/codegen/lower_inst/builtins/strings.rs +++ b/src/codegen/lower_inst/builtins/strings.rs @@ -19,7 +19,7 @@ use crate::types::PhpType; use super::super::super::context::FunctionContext; use super::super::predicates; use super::{ - ensure_arg_count, ensure_arg_count_between, expect_operand, io, + expect_operand, io, load_value_to_first_int_arg, store_if_result, }; @@ -75,7 +75,7 @@ pub(crate) use search::{ }; pub(crate) use simple::{ lower_binary_string_runtime, lower_grapheme_strrev, lower_html_escape, lower_lcfirst, - lower_trim_like, lower_ucfirst, lower_unary_string_runtime, + lower_trim_like, lower_ucfirst, }; pub(crate) use split::{lower_explode, lower_implode, lower_sscanf, lower_str_split}; @@ -87,3 +87,296 @@ pub(super) use common::{ pub(super) use printf::{ pack_sprintf_like_arg, sprintf_spec_cats_for_format, SprintfSpecCat, }; + +/// Materializes the optional `explode()` `$limit` into the splitter's extra argument register. +/// +/// The already-materialized separator/subject pairs are parked while `$limit` is evaluated, +/// because coercing a non-integer limit can call runtime helpers that clobber the very +/// argument registers those pairs occupy. An omitted `$limit` becomes `PHP_INT_MAX`, which is +/// exactly how php-src spells "no limit" and lets the runtime helper share one code path. +fn load_split_limit_arg( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, +) -> Result<()> { + let limit_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x5", + Arch::X86_64 => "rcx", + }; + if inst.operands.len() < 3 { + abi::emit_load_int_immediate(ctx.emitter, limit_reg, i64::MAX); + return Ok(()); + } + let limit = expect_operand(inst, 2)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // park the separator while the limit is materialized + ctx.emitter.instruction("stp x3, x4, [sp, #-16]!"); // park the subject string while the limit is materialized + load_as_int(ctx, limit, &format!("{} limit", name))?; + ctx.emitter.instruction("mov x5, x0"); // pass the element limit as the extra splitter argument + ctx.emitter.instruction("ldp x3, x4, [sp], #16"); // restore the subject string into its splitter argument registers + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the separator into its splitter argument registers + } + Arch::X86_64 => { + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); // park the separator while the limit is materialized + abi::emit_push_reg_pair(ctx.emitter, "rdi", "rsi"); // park the subject string while the limit is materialized + load_as_int(ctx, limit, &format!("{} limit", name))?; + ctx.emitter.instruction("mov rcx, rax"); // pass the element limit as the extra splitter argument + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); // restore the subject string into its splitter argument registers + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); // restore the separator into its splitter argument registers + } + } + Ok(()) +} + +/// php-src's verbatim `ValueError` wording for a `base_convert()` `$from_base` outside 2..36. +const BASE_CONVERT_FROM_BASE_MESSAGE: &str = + "base_convert(): Argument #2 ($from_base) must be between 2 and 36 (inclusive)"; + +/// php-src's verbatim `ValueError` wording for a `base_convert()` `$to_base` outside 2..36. +const BASE_CONVERT_TO_BASE_MESSAGE: &str = + "base_convert(): Argument #3 ($to_base) must be between 2 and 36 (inclusive)"; + +/// php-src's verbatim `ValueError` wording for `chunk_split()` with a non-positive `$length`. +const CHUNK_SPLIT_NON_POSITIVE_LENGTH_MESSAGE: &str = + "chunk_split(): Argument #2 ($length) must be greater than 0"; + +/// php-src's verbatim `ValueError` wording for `count_chars()` with an unknown `$mode`. +const COUNT_CHARS_MODE_MESSAGE: &str = + "count_chars(): Argument #2 ($mode) must be between 0 and 4 (inclusive)"; + +/// php-src's verbatim `ValueError` wording for `explode()` with an empty `$separator`. +const EXPLODE_EMPTY_SEPARATOR_MESSAGE: &str = + "explode(): Argument #1 ($separator) must not be empty"; + +/// php-src's verbatim `ValueError` wording, minus the leading function name, for a +/// `strpos()`-family `$offset` that does not land inside the haystack. +/// +/// php-src emits the same sentence for `strpos()` and `strrpos()`, differing only in the +/// function name it is prefixed with, so the shared suffix is stored once and the caller +/// supplies the PHP spelling of the builtin being lowered. +const STRING_POSITION_OFFSET_OUT_OF_RANGE_SUFFIX: &str = + "(): Argument #3 ($offset) must be contained in argument #1 ($haystack)"; + +/// php-src's verbatim `ValueError` wording for `strncasecmp()` with a negative `$length`. +const STRNCASECMP_NEGATIVE_LENGTH_MESSAGE: &str = + "strncasecmp(): Argument #3 ($length) must be greater than or equal to 0"; + +/// php-src's verbatim `ValueError` wording for `strncmp()` with a negative `$length`. +const STRNCMP_NEGATIVE_LENGTH_MESSAGE: &str = + "strncmp(): Argument #3 ($length) must be greater than or equal to 0"; + +/// php-src's verbatim `ValueError` wording for `str_repeat()` with a negative `$times`. +const STR_REPEAT_NEGATIVE_TIMES_MESSAGE: &str = + "str_repeat(): Argument #2 ($times) must be greater than or equal to 0"; + +/// php-src's verbatim `ValueError` wording for `str_split()` with a non-positive `$length`. +const STR_SPLIT_NON_POSITIVE_LENGTH_MESSAGE: &str = + "str_split(): Argument #2 ($length) must be greater than 0"; + +/// php-src's verbatim `ValueError` wording for `str_word_count()` with an unknown `$format`. +const STR_WORD_COUNT_FORMAT_MESSAGE: &str = + "str_word_count(): Argument #2 ($format) must be a valid format value"; + +/// php-src's verbatim `ValueError` wording for `substr_count()` with an empty `$needle`. +const SUBSTR_COUNT_EMPTY_NEEDLE_MESSAGE: &str = + "substr_count(): Argument #2 ($needle) must not be empty"; + +/// php-src's verbatim `ValueError` wording for a `substr_count()` `$offset` outside the subject. +const SUBSTR_COUNT_OFFSET_OUT_OF_RANGE_MESSAGE: &str = + "substr_count(): Argument #3 ($offset) must be contained in argument #1 ($haystack)"; + +/// The scan direction of a `strpos()`-family builtin, which decides how `$offset` bounds +/// the searched window. +/// +/// PHP resolves the third argument differently for the two directions: `strpos()` always +/// turns it into the first byte it may match at, while `strrpos()` turns a negative value +/// into the last byte a match may *end* on. Both spellings share one lowering, so the +/// direction is carried explicitly rather than re-derived from the runtime symbol name. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum StringPositionDirection { + /// Left-to-right search (`strpos()`). + Forward, + /// Right-to-left search (`strrpos()`). + Reverse, +} + +/// php-src's verbatim `ValueError` wording for `wordwrap()` with an empty `$break`. +const WORDWRAP_EMPTY_BREAK_MESSAGE: &str = + "wordwrap(): Argument #3 ($break) must not be empty"; + +/// php-src's verbatim `ValueError` wording for a zero-width cutting `wordwrap()`. +const WORDWRAP_ZERO_WIDTH_CUT_MESSAGE: &str = + "wordwrap(): Argument #4 ($cut_long_words) cannot be true when argument #2 ($width) is 0"; + +/// Rejects the `str_pad()` argument values reference PHP refuses to pad with. +/// +/// `__rt_str_pad` copies `length - strlen($string)` bytes out of the pad string, so an +/// empty `$pad_string` would make it read whatever happens to follow the zero-length +/// buffer — that is the uninitialized `"xUUU"` output this guard removes. php-src checks +/// in exactly this order: a `$length` that cannot grow the input returns the input +/// untouched *before* either value check, then `$pad_string` emptiness, then `$pad_type`. +/// `has_pad_type` suppresses the fourth-argument guard for calls that leave `$pad_type` +/// defaulted, where `STR_PAD_RIGHT` is materialized as a constant and can never fail. +fn emit_str_pad_argument_guards(ctx: &mut FunctionContext<'_>, has_pad_type: bool) { + let ok_label = ctx.next_label("str_pad_args_ok"); + let empty_pad_label = ctx.next_label("str_pad_empty_pad_string"); + let bad_type_label = ctx.next_label("str_pad_bad_pad_type"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x5, x2"); // compare the requested length against the input length + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // PHP returns the input unchanged before validating anything else + ctx.emitter.instruction(&format!("cbz x4, {}", empty_pad_label)); // an empty pad string cannot supply the missing bytes + if has_pad_type { + ctx.emitter.instruction("cmp x7, #2"); // STR_PAD_LEFT/RIGHT/BOTH occupy 0..2 + ctx.emitter.instruction(&format!("b.hi {}", bad_type_label)); // any other pad mode, including negatives, is rejected + } + ctx.emitter.instruction(&format!("b {}", ok_label)); // both padding arguments are usable, so run the helper + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rcx, rdx"); // compare the requested length against the input length + ctx.emitter.instruction(&format!("jle {}", ok_label)); // PHP returns the input unchanged before validating anything else + ctx.emitter.instruction("test rsi, rsi"); // is the pad string empty? + ctx.emitter.instruction(&format!("jz {}", empty_pad_label)); // an empty pad string cannot supply the missing bytes + if has_pad_type { + ctx.emitter.instruction("cmp r8, 2"); // STR_PAD_LEFT/RIGHT/BOTH occupy 0..2 + ctx.emitter.instruction(&format!("ja {}", bad_type_label)); // any other pad mode, including negatives, is rejected + } + ctx.emitter.instruction(&format!("jmp {}", ok_label)); // both padding arguments are usable, so run the helper + } + } + ctx.emitter.label(&empty_pad_label); + super::super::exceptions::emit_value_error(ctx, STR_PAD_EMPTY_PAD_STRING_MESSAGE); + if has_pad_type { + ctx.emitter.label(&bad_type_label); + super::super::exceptions::emit_value_error(ctx, STR_PAD_INVALID_PAD_TYPE_MESSAGE); + } + ctx.emitter.label(&ok_label); +} + + + +/// Lowers `base64_decode(string, strict?)` and boxes its `string|false` answer as Mixed. +/// +/// `__rt_base64_decode` reports a strict-mode rejection out of band — the decoded string +/// pair plus a separate success flag — because PHP's `false` and a successfully decoded +/// empty string are two different values that share the same empty pointer/length pair. +/// Both arms are boxed here, so the caller always receives one `Mixed` cell. +pub(crate) fn lower_base64_decode(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.is_empty() || inst.operands.len() > 2 { + return Err(CodegenIrError::invalid_module(format!( + "base64_decode expected 1 or 2 args, got {}", + inst.operands.len() + ))); + } + if inst.result.is_some() && inst.result_php_type.codegen_repr() != PhpType::Mixed { + // `crate::builtins::string::base64_decode::check` types EVERY call `string|false`, + // whose representation is `Mixed`, and both arms below leave a BOXED cell in the + // integer result register. A `Str` result type here would make `store_if_result` copy + // the string-pair registers instead, which no longer hold the answer. + return Err(CodegenIrError::invalid_module(format!( + "base64_decode result must be Mixed (string|false), got {:?}", + inst.result_php_type + ))); + } + let false_label = ctx.next_label("base64_decode_false"); + let end_label = ctx.next_label("base64_decode_end"); + // `$strict` is materialized FIRST and parked on the temporary stack: the truthiness + // helpers clobber the same caller-saved registers the subject materialization needs. + materialize_truthy_flag(ctx, inst, 1, "base64_decode")?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "base64_decode", "x1", "x2")?; + abi::emit_pop_reg(ctx.emitter, "x3"); // reload the parked $strict flag into the decoder's flag argument + abi::emit_call_label(ctx.emitter, "__rt_base64_decode"); + ctx.emitter.instruction(&format!("cbz x0, {}", false_label)); // a strict decode that hit a bad character returns PHP's false + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "base64_decode", "rax", "rdx")?; + abi::emit_pop_reg(ctx.emitter, "rdi"); // reload the parked $strict flag into the decoder's flag argument + abi::emit_call_label(ctx.emitter, "__rt_base64_decode"); + ctx.emitter.instruction("test r8, r8"); // did the decoder accept the encoded input? + ctx.emitter.instruction(&format!("jz {}", false_label)); // a strict decode that hit a bad character returns PHP's false + } + } + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Str); + match ctx.emitter.target.arch { + Arch::AArch64 => ctx.emitter.instruction(&format!("b {}", end_label)), // skip the false arm once the decoded string is boxed + Arch::X86_64 => ctx.emitter.instruction(&format!("jmp {}", end_label)), // skip the false arm once the decoded string is boxed + } + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + crate::codegen::emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + ctx.emitter.label(&end_label); + store_if_result(ctx, inst) +} + +/// Lowers `ucwords(string, separators?)` with an explicit separator byte set. +/// +/// `__rt_ucwords` always scans a caller-supplied set, so an omitted `$separators` is +/// materialized here as the address of `_ucwords_default_seps`. That keeps PHP's default +/// (`" \t\r\n\f\v"`, including the `\r`, `\f`, and `\v` the old hard-coded scan missed) and an +/// explicitly written set on exactly the same runtime path. +pub(crate) fn lower_ucwords(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.is_empty() || inst.operands.len() > 2 { + return Err(CodegenIrError::invalid_module(format!( + "ucwords expected 1 or 2 args, got {}", + inst.operands.len() + ))); + } + let (subject_ptr, subject_len, sep_ptr, sep_len) = match ctx.emitter.target.arch { + Arch::AArch64 => ("x1", "x2", "x3", "x4"), + Arch::X86_64 => ("rdi", "rsi", "rdx", "rcx"), + }; + if inst.operands.len() == 2 { + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "ucwords", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while the separator set is materialized + load_string_arg_to_regs(ctx, inst, 1, "ucwords", "x3", "x4")?; + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime string argument + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "ucwords", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_string_arg_to_regs(ctx, inst, 1, "ucwords", "rax", "rdx")?; + ctx.emitter.instruction("mov rcx, rdx"); // pass the separator length as the fourth SysV argument + ctx.emitter.instruction("mov rdx, rax"); // pass the separator pointer as the third SysV argument + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); // restore the subject into the primary SysV string arguments + } + } + } else { + load_string_arg_to_regs(ctx, inst, 0, "ucwords", subject_ptr, subject_len)?; + abi::emit_symbol_address(ctx.emitter, sep_ptr, "_ucwords_default_seps"); + abi::emit_load_int_immediate(ctx.emitter, sep_len, UCWORDS_DEFAULT_SEPARATOR_COUNT); + } + abi::emit_call_label(ctx.emitter, "__rt_ucwords"); + store_if_result(ctx, inst) +} + + +/// php-src's verbatim `ValueError` wording for `str_pad()` with an empty `$pad_string`. +const STR_PAD_EMPTY_PAD_STRING_MESSAGE: &str = + "str_pad(): Argument #3 ($pad_string) must not be empty"; + +/// php-src's verbatim `ValueError` wording for a `str_pad()` `$pad_type` outside 0..2. +const STR_PAD_INVALID_PAD_TYPE_MESSAGE: &str = + "str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH"; + +/// The byte length of `_ucwords_default_seps`, PHP's `" \t\r\n\f\v"` separator set. +const UCWORDS_DEFAULT_SEPARATOR_COUNT: i64 = 6; +pub(crate) use scalar::load_as_int; +pub(crate) use replace_wrap::lower_base_convert; +pub(crate) use split::lower_base_to_number; +pub(crate) use replace_wrap::lower_chunk_split; +pub(crate) use replace_wrap::lower_count_chars; +pub(crate) use split::lower_dec_to_base; +pub(crate) use split::lower_length_limited_compare; +pub(crate) use replace_wrap::lower_str_word_count; +pub(crate) use replace_wrap::lower_strtr; +pub(crate) use search::lower_substr_count; + +/// php-src's verbatim `ValueError` wording for a `substr_count()` `$length` outside the subject. +const SUBSTR_COUNT_LENGTH_OUT_OF_RANGE_MESSAGE: &str = + "substr_count(): Argument #4 ($length) must be contained in argument #1 ($haystack)"; diff --git a/src/codegen/lower_inst/builtins/strings/common.rs b/src/codegen/lower_inst/builtins/strings/common.rs index c6abb6912d..b6b253cbac 100644 --- a/src/codegen/lower_inst/builtins/strings/common.rs +++ b/src/codegen/lower_inst/builtins/strings/common.rs @@ -75,9 +75,9 @@ pub(super) fn load_binary_string_args( inst: &Instruction, name: &str, ) -> Result<()> { - if inst.operands.len() != 2 { + if inst.operands.len() < 2 || inst.operands.len() > 3 { return Err(CodegenIrError::invalid_module(format!( - "{} expected 2 args, got {}", + "{} expected 2 or 3 args, got {}", name, inst.operands.len() ))); diff --git a/src/codegen/lower_inst/builtins/strings/hash.rs b/src/codegen/lower_inst/builtins/strings/hash.rs index 96b78dfdd6..dd9bd658b4 100644 --- a/src/codegen/lower_inst/builtins/strings/hash.rs +++ b/src/codegen/lower_inst/builtins/strings/hash.rs @@ -68,7 +68,7 @@ pub(crate) fn lower_hash_algos(ctx: &mut FunctionContext<'_>, inst: &Instruction /// Lowers `hash_init(algo)` and returns a boxed HashContext resource. pub(crate) fn lower_hash_init(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "hash_init", 1)?; + super::super::ensure_arg_count(inst, "hash_init", 1)?; load_string_arg_to_regs(ctx, inst, 0, "hash_init", string_ptr_reg(ctx), string_len_reg(ctx))?; crate::codegen::hash_crypto::publish_elephc_crypto_function_pointers( ctx.emitter, @@ -79,7 +79,7 @@ pub(crate) fn lower_hash_init(ctx: &mut FunctionContext<'_>, inst: &Instruction) /// Lowers `hash_update(context, data)` through the incremental hash runtime helper. pub(crate) fn lower_hash_update(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "hash_update", 2)?; + super::super::ensure_arg_count(inst, "hash_update", 2)?; let context = expect_operand(inst, 0)?; super::io::load_stream_fd_to_result(ctx, context, "hash_update")?; match ctx.emitter.target.arch { @@ -135,7 +135,7 @@ pub(crate) fn lower_hash_final(ctx: &mut FunctionContext<'_>, inst: &Instruction /// Lowers `hash_copy(context)` through the incremental hash clone helper. pub(crate) fn lower_hash_copy(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "hash_copy", 1)?; + super::super::ensure_arg_count(inst, "hash_copy", 1)?; let context = expect_operand(inst, 0)?; super::io::load_stream_fd_to_result(ctx, context, "hash_copy")?; if ctx.emitter.target.arch == Arch::X86_64 { @@ -159,7 +159,7 @@ pub(crate) fn lower_crc32(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> /// /// Omitted/null encodings use a null pointer plus zero length; explicit names stay byte strings for PHP-compatible case-insensitive lookup and `ValueError` handling. pub(crate) fn lower_mb_strlen(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count_between(inst, "mb_strlen", 1, 2)?; + super::super::ensure_arg_count_between(inst, "mb_strlen", 1, 2)?; match ctx.emitter.target.arch { Arch::AArch64 => { load_string_arg_to_regs(ctx, inst, 0, "mb_strlen", "x1", "x2")?; diff --git a/src/codegen/lower_inst/builtins/strings/network.rs b/src/codegen/lower_inst/builtins/strings/network.rs index 06e3805776..524e91a1af 100644 --- a/src/codegen/lower_inst/builtins/strings/network.rs +++ b/src/codegen/lower_inst/builtins/strings/network.rs @@ -11,7 +11,7 @@ use super::*; /// Lowers `long2ip(value)` through the IPv4 formatting runtime helper. pub(crate) fn lower_long2ip(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "long2ip", 1)?; + super::super::ensure_arg_count(inst, "long2ip", 1)?; let value = expect_operand(inst, 0)?; load_as_int(ctx, value, "long2ip")?; if ctx.emitter.target.arch == Arch::X86_64 { diff --git a/src/codegen/lower_inst/builtins/strings/parse_url.rs b/src/codegen/lower_inst/builtins/strings/parse_url.rs index 44c646074f..1ef15eefde 100644 --- a/src/codegen/lower_inst/builtins/strings/parse_url.rs +++ b/src/codegen/lower_inst/builtins/strings/parse_url.rs @@ -12,7 +12,7 @@ use super::*; /// Lowers `parse_url(url, component?)` into the Mixed-returning runtime scanner. pub(crate) fn lower_parse_url(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count_between(inst, "parse_url", 1, 2)?; + super::super::ensure_arg_count_between(inst, "parse_url", 1, 2)?; let ptr_reg = string_ptr_reg(ctx); let len_reg = string_len_reg(ctx); load_string_arg_to_regs(ctx, inst, 0, "parse_url url", ptr_reg, len_reg)?; diff --git a/src/codegen/lower_inst/builtins/strings/printf.rs b/src/codegen/lower_inst/builtins/strings/printf.rs index 7ca4b5b7ce..1925216a9d 100644 --- a/src/codegen/lower_inst/builtins/strings/printf.rs +++ b/src/codegen/lower_inst/builtins/strings/printf.rs @@ -107,8 +107,44 @@ pub(in crate::codegen::lower_inst::builtins) fn sprintf_spec_cats_for_format( } /// Parses the conversion categories consumed by the runtime sprintf scanner. +/// Highest `printf`-family argument position `parse_sprintf_spec_cats` will track. A format +/// string is program text, so its `N$` digits are attacker-controlled; the cap keeps the +/// category table from being sized by them. Positions above it fall back to static-type +/// packing and are rejected by the runtime's argument-count check. +const MAX_TRACKED_SPRINTF_ARGS: usize = 4096; + + + + + + + + + + + + + + + + + + + +/// Parses the conversion categories consumed by the runtime sprintf scanner, indexed by the +/// argument position each conversion consumes. +/// +/// The result must agree with `__rt_sprintf`'s own specifier parser, because the runtime +/// dispatches on the conversion character while this pass decides how the operand is +/// coerced and tagged. That means recognizing everything the runtime recognizes: PHP's +/// `N$` explicit argument numbers (which select a position without advancing the sequential +/// cursor, exactly like PHP), the `'X` custom-pad-character flag (whose `X` must not be +/// mistaken for the conversion character), and the full float conversion set +/// `f F e E g G`. Positions no conversion refers to keep an inert `Str` coercion; the +/// runtime never reads those records. pub(super) fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { - let mut cats = Vec::new(); + let mut cats: Vec> = Vec::new(); + let mut next_arg = 0usize; let mut index = 0; while index < format.len() { if format[index] != b'%' { @@ -123,10 +159,27 @@ pub(super) fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { index += 1; continue; } - while index < format.len() - && matches!(format[index], b'-' | b'+' | b'0' | b' ' | b'#') - { - index += 1; + let mut explicit: Option = None; + let mut probe = index; + while probe < format.len() && format[probe].is_ascii_digit() { + probe += 1; + } + if probe > index && probe < format.len() && format[probe] == b'$' { + let mut value: usize = 0; + for digit in &format[index..probe] { + value = value + .saturating_mul(10) + .saturating_add((digit - b'0') as usize); + } + explicit = Some(value); + index = probe + 1; + } + while index < format.len() { + match format[index] { + b'-' | b'+' | b'0' | b' ' | b'#' => index += 1, + b'\'' => index += 2, + _ => break, + } } while index < format.len() && format[index].is_ascii_digit() { index += 1; @@ -140,14 +193,33 @@ pub(super) fn parse_sprintf_spec_cats(format: &[u8]) -> Vec { if index >= format.len() { break; } - cats.push(match format[index] { - b'f' | b'e' | b'g' => SprintfSpecCat::Float, + let cat = match format[index] { + b'f' | b'F' | b'e' | b'E' | b'g' | b'G' => SprintfSpecCat::Float, b's' => SprintfSpecCat::Str, _ => SprintfSpecCat::Int, - }); + }; index += 1; + let slot = match explicit { + // `%0$s` has no operand, and an argument number far past any real call cannot + // match one either. Both are left for the runtime's argument-count check rather + // than sizing this table from an attacker-controlled digit run. + Some(0) => continue, + Some(number) if number > MAX_TRACKED_SPRINTF_ARGS => continue, + Some(number) => number - 1, + None => { + let slot = next_arg; + next_arg += 1; + slot + } + }; + if slot >= cats.len() { + cats.resize(slot + 1, None); + } + cats[slot] = Some(cat); } - cats + cats.into_iter() + .map(|cat| cat.unwrap_or(SprintfSpecCat::Str)) + .collect() } /// Preserves the format string, evaluates the values array, and calls `__rt_vsprintf`. diff --git a/src/codegen/lower_inst/builtins/strings/replace_wrap.rs b/src/codegen/lower_inst/builtins/strings/replace_wrap.rs index 01dc656156..7c61ac3a5a 100644 --- a/src/codegen/lower_inst/builtins/strings/replace_wrap.rs +++ b/src/codegen/lower_inst/builtins/strings/replace_wrap.rs @@ -43,10 +43,417 @@ pub(crate) fn lower_wordwrap(ctx: &mut FunctionContext<'_>, inst: &Instruction) Arch::AArch64 => lower_wordwrap_aarch64(ctx, inst)?, Arch::X86_64 => lower_wordwrap_x86_64(ctx, inst)?, } + emit_wordwrap_argument_guards(ctx); abi::emit_call_label(ctx.emitter, "__rt_wordwrap"); store_if_result(ctx, inst) } +/// Rejects the `wordwrap()` argument values reference PHP refuses to wrap with. +/// +/// An empty `$break` gives the wrapper nothing to insert, so it silently returned the input +/// unwrapped where PHP raises a `ValueError`; a zero `$width` combined with `$cut_long_words` +/// asks for progress-free cutting. php-src checks `$break` first, then the width/cut pair. +fn emit_wordwrap_argument_guards(ctx: &mut FunctionContext<'_>) { + let break_ok_label = ctx.next_label("wordwrap_break_ok"); + let width_ok_label = ctx.next_label("wordwrap_width_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbnz x5, {}", break_ok_label)); // a non-empty break string can be inserted + } + Arch::X86_64 => { + ctx.emitter.instruction("test r8, r8"); // is the break string empty? + ctx.emitter.instruction(&format!("jnz {}", break_ok_label)); // a non-empty break string can be inserted + } + } + super::super::exceptions::emit_value_error(ctx, WORDWRAP_EMPTY_BREAK_MESSAGE); + ctx.emitter.label(&break_ok_label); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbnz x3, {}", width_ok_label)); // a non-zero width always makes progress + ctx.emitter.instruction(&format!("cbz x6, {}", width_ok_label)); // a zero width is only rejected together with $cut_long_words + } + Arch::X86_64 => { + ctx.emitter.instruction("test rdi, rdi"); // is the requested wrap width zero? + ctx.emitter.instruction(&format!("jnz {}", width_ok_label)); // a non-zero width always makes progress + ctx.emitter.instruction("test r9, r9"); // was $cut_long_words requested? + ctx.emitter.instruction(&format!("jz {}", width_ok_label)); // a zero width is only rejected together with $cut_long_words + } + } + super::super::exceptions::emit_value_error(ctx, WORDWRAP_ZERO_WIDTH_CUT_MESSAGE); + ctx.emitter.label(&width_ok_label); +} + +/// Lowers `base_convert(num, from_base, to_base)` through the shared runtime helper. +/// +/// php-src validates `$from_base` first and `$to_base` second, before touching `$num`, so the +/// two guards are emitted in that order once both bases sit in their runtime argument +/// registers. +pub(crate) fn lower_base_convert(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.len() != 3 { + return Err(CodegenIrError::invalid_module(format!( + "base_convert expected 3 args, got {}", + inst.operands.len() + ))); + } + match ctx.emitter.target.arch { + Arch::AArch64 => lower_base_convert_aarch64(ctx, inst)?, + Arch::X86_64 => lower_base_convert_x86_64(ctx, inst)?, + } + let (from_base_reg, to_base_reg) = match ctx.emitter.target.arch { + Arch::AArch64 => ("x3", "x4"), + Arch::X86_64 => ("rdx", "rcx"), + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedInRange(from_base_reg, 2, 36), + BASE_CONVERT_FROM_BASE_MESSAGE, + ); + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedInRange(to_base_reg, 2, 36), + BASE_CONVERT_TO_BASE_MESSAGE, + ); + abi::emit_call_label(ctx.emitter, "__rt_base_convert"); + store_if_result(ctx, inst) +} + +/// Materializes AArch64 `base_convert()` runtime arguments. +/// +/// Both bases are materialized after the numeral, so each one is parked on the stack while +/// the next operand is lowered: `load_as_int` may call `__rt_str_to_int`, which clobbers +/// every scratch register the earlier arguments were sitting in. +fn lower_base_convert_aarch64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + load_string_arg_to_regs(ctx, inst, 0, "base_convert", "x1", "x2")?; + abi::emit_push_reg_pair(ctx.emitter, "x1", "x2"); + let from_base = expect_operand(inst, 1)?; + load_as_int(ctx, from_base, "base_convert from_base")?; + abi::emit_push_reg_pair(ctx.emitter, "x0", "xzr"); + let to_base = expect_operand(inst, 2)?; + load_as_int(ctx, to_base, "base_convert to_base")?; + ctx.emitter.instruction("mov x4, x0"); // pass the target base to the runtime helper + abi::emit_pop_reg_pair(ctx.emitter, "x3", "x9"); + abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + Ok(()) +} + +/// Materializes x86_64 `base_convert()` runtime arguments. +/// +/// Same staging as the AArch64 path: the numeral and the source base wait on the stack until +/// the target base has been materialized, then everything lands in the System V registers +/// `__rt_base_convert` reads. +fn lower_base_convert_x86_64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + load_string_arg_to_regs(ctx, inst, 0, "base_convert", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + let from_base = expect_operand(inst, 1)?; + load_as_int(ctx, from_base, "base_convert from_base")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rax"); + let to_base = expect_operand(inst, 2)?; + load_as_int(ctx, to_base, "base_convert to_base")?; + ctx.emitter.instruction("mov rcx, rax"); // pass the target base to the runtime helper + abi::emit_pop_reg_pair(ctx.emitter, "rdx", "r9"); + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); + Ok(()) +} + +/// Lowers `chunk_split(string, length?, separator?)` through the shared runtime helper. +pub(crate) fn lower_chunk_split(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.is_empty() || inst.operands.len() > 3 { + return Err(CodegenIrError::invalid_module(format!( + "chunk_split expected 1 to 3 args, got {}", + inst.operands.len() + ))); + } + match ctx.emitter.target.arch { + Arch::AArch64 => lower_chunk_split_aarch64(ctx, inst)?, + Arch::X86_64 => lower_chunk_split_x86_64(ctx, inst)?, + } + // `__rt_chunk_split` divides the subject length by the chunk length, so a zero length + // would trap and a negative one would make the unsigned compare copy the whole subject + // forever. Reference PHP rejects both before touching the subject. + let length_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedAtLeast(length_reg, 1), + CHUNK_SPLIT_NON_POSITIVE_LENGTH_MESSAGE, + ); + abi::emit_call_label(ctx.emitter, "__rt_chunk_split"); + store_if_result(ctx, inst) +} + +/// Materializes AArch64 `chunk_split()` runtime arguments. +fn lower_chunk_split_aarch64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let subject = expect_string_operand(ctx, inst, 0, "chunk_split")?; + ctx.load_string_value_to_regs(subject, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while materializing the length and separator + if inst.operands.len() >= 2 { + let length = expect_operand(inst, 1)?; + load_as_int(ctx, length, "chunk_split length")?; + ctx.emitter.instruction("mov x3, x0"); // pass the requested chunk length to the runtime helper + } else { + ctx.emitter.instruction("mov x3, #76"); // use PHP's default 76-byte chunk length when omitted + } + if inst.operands.len() >= 3 { + let separator = expect_string_operand(ctx, inst, 2, "chunk_split")?; + ctx.load_string_value_to_regs(separator, "x1", "x2")?; + ctx.emitter.instruction("mov x4, x1"); // pass the separator pointer to the runtime helper + ctx.emitter.instruction("mov x5, x2"); // pass the separator length to the runtime helper + } else { + let (label, len) = ctx.data.add_string(b"\r\n"); + abi::emit_symbol_address(ctx.emitter, "x4", &label); + abi::emit_load_int_immediate(ctx.emitter, "x5", len as i64); + } + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime argument registers + Ok(()) +} + +/// Materializes x86_64 `chunk_split()` runtime arguments. +fn lower_chunk_split_x86_64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let subject = expect_string_operand(ctx, inst, 0, "chunk_split")?; + ctx.load_string_value_to_regs(subject, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + if inst.operands.len() >= 2 { + let length = expect_operand(inst, 1)?; + load_as_int(ctx, length, "chunk_split length")?; + ctx.emitter.instruction("mov rdi, rax"); // pass the requested chunk length to the runtime helper + } else { + ctx.emitter.instruction("mov rdi, 76"); // use PHP's default 76-byte chunk length when omitted + } + if inst.operands.len() >= 3 { + let separator = expect_string_operand(ctx, inst, 2, "chunk_split")?; + ctx.load_string_value_to_regs(separator, "rax", "rdx")?; + ctx.emitter.instruction("mov rcx, rax"); // pass the separator pointer to the runtime helper + ctx.emitter.instruction("mov r8, rdx"); // pass the separator length to the runtime helper + } else { + let (label, len) = ctx.data.add_string(b"\r\n"); + abi::emit_symbol_address(ctx.emitter, "rcx", &label); + abi::emit_load_int_immediate(ctx.emitter, "r8", len as i64); + } + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + Ok(()) +} + +/// Lowers both `strtr()` shapes through their shared runtime helpers. +/// +/// The form is selected from the STATIC type of `$from`, not from the operand count: a named +/// `strtr(string: $s, from: [...])` call still materializes the trailing `$to` default, so an +/// array `$from` always means the replacement-pair form. Its container shape then picks +/// between the hash helper and the indexed-array wrapper that converts before replacing. +pub(crate) fn lower_strtr(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::super::ensure_arg_count_between(inst, "strtr", 2, 3)?; + let pairs = expect_operand(inst, 1)?; + let helper = match ctx.value_php_type(pairs)? { + PhpType::AssocArray { .. } => "__rt_strtr_hash", + PhpType::Array(_) => "__rt_strtr_array", + _ => return lower_strtr_pairwise(ctx, inst), + }; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "strtr", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while materializing the replacement pairs + ctx.load_value_to_result(pairs)?; + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime argument registers + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "strtr", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + ctx.load_value_to_result(pairs)?; + ctx.emitter.instruction("mov rdi, rax"); // pass the replacement pairs to the runtime helper + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + } + } + abi::emit_call_label(ctx.emitter, helper); + store_if_result(ctx, inst) +} + +/// Materializes the three-argument `strtr($string, $from, $to)` byte-translation form. +/// +/// A missing or `null` `$to` yields a zero-length destination list, which makes the mapping +/// empty and leaves the subject untouched — the same result php-src produces for +/// `strtr($s, $from, null)` after its deprecation notice. +fn lower_strtr_pairwise(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "strtr", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while materializing the byte lists + load_string_arg_to_regs(ctx, inst, 1, "strtr", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the source byte list while materializing the destination list + load_optional_strtr_to(ctx, inst, "x5", "x6")?; + ctx.emitter.instruction("ldp x3, x4, [sp], #16"); // restore the source byte list into the runtime argument registers + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime argument registers + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "strtr", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_string_arg_to_regs(ctx, inst, 1, "strtr", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_optional_strtr_to(ctx, inst, "rcx", "r8")?; + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + } + } + abi::emit_call_label(ctx.emitter, "__rt_strtr_pairwise"); + store_if_result(ctx, inst) +} + +/// Loads the nullable `strtr()` `$to` byte list into a pointer/length pair. +fn load_optional_strtr_to( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + ptr_reg: &str, + len_reg: &str, +) -> Result<()> { + let Some(to) = inst.operands.get(2).copied() else { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + }; + if matches!(ctx.value_php_type(to)?, PhpType::Void | PhpType::Never) { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + } + load_value_as_string_to_regs(ctx, to, "strtr to", ptr_reg, len_reg) +} + +/// Lowers `count_chars(string, mode?)` through the shared runtime helper. +/// +/// The checker already fixed the result storage from the literal `$mode`, so the only runtime +/// validation left is php-src's `ValueError` for a mode outside `0..=4`, raised before +/// `__rt_count_chars` allocates anything. +pub(crate) fn lower_count_chars(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::super::ensure_arg_count_between(inst, "count_chars", 1, 2)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, "count_chars", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while materializing the mode + if inst.operands.len() >= 2 { + let mode = expect_operand(inst, 1)?; + load_as_int(ctx, mode, "count_chars mode")?; + ctx.emitter.instruction("mov x3, x0"); // pass the requested result mode to the runtime helper + } else { + ctx.emitter.instruction("mov x3, xzr"); // php's default mode 0 tallies every byte value + } + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime argument registers + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, "count_chars", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + if inst.operands.len() >= 2 { + let mode = expect_operand(inst, 1)?; + load_as_int(ctx, mode, "count_chars mode")?; + ctx.emitter.instruction("mov rdi, rax"); // pass the requested result mode to the runtime helper + } else { + ctx.emitter.instruction("xor edi, edi"); // php's default mode 0 tallies every byte value + } + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + } + } + let mode_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedInRange(mode_reg, 0, 4), + COUNT_CHARS_MODE_MESSAGE, + ); + abi::emit_call_label(ctx.emitter, "__rt_count_chars"); + store_if_result(ctx, inst) +} + +/// Lowers `str_word_count(string, format?, characters?)` through the shared runtime helper. +/// +/// The checker already fixed the result storage from the literal `$format`, so the only +/// runtime validation left is php-src's `ValueError` for a format outside `0..=2`, raised +/// before `__rt_str_word_count` allocates anything. +pub(crate) fn lower_str_word_count( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::super::ensure_arg_count_between(inst, "str_word_count", 1, 3)?; + match ctx.emitter.target.arch { + Arch::AArch64 => lower_str_word_count_aarch64(ctx, inst)?, + Arch::X86_64 => lower_str_word_count_x86_64(ctx, inst)?, + } + let format_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedInRange(format_reg, 0, 2), + STR_WORD_COUNT_FORMAT_MESSAGE, + ); + abi::emit_call_label(ctx.emitter, "__rt_str_word_count"); + store_if_result(ctx, inst) +} + +/// Materializes AArch64 `str_word_count()` runtime arguments. +fn lower_str_word_count_aarch64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let subject = expect_string_operand(ctx, inst, 0, "str_word_count")?; + ctx.load_string_value_to_regs(subject, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject while materializing the format and character list + if inst.operands.len() >= 2 { + let format = expect_operand(inst, 1)?; + load_as_int(ctx, format, "str_word_count format")?; + ctx.emitter.instruction("mov x3, x0"); // pass the requested result format to the runtime helper + } else { + ctx.emitter.instruction("mov x3, xzr"); // php's default format 0 returns the plain word count + } + load_optional_str_word_count_characters(ctx, inst, "x4", "x5")?; + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime argument registers + Ok(()) +} + +/// Materializes x86_64 `str_word_count()` runtime arguments. +fn lower_str_word_count_x86_64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let subject = expect_string_operand(ctx, inst, 0, "str_word_count")?; + ctx.load_string_value_to_regs(subject, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + if inst.operands.len() >= 2 { + let format = expect_operand(inst, 1)?; + load_as_int(ctx, format, "str_word_count format")?; + ctx.emitter.instruction("mov rdi, rax"); // pass the requested result format to the runtime helper + } else { + ctx.emitter.instruction("xor edi, edi"); // php's default format 0 returns the plain word count + } + load_optional_str_word_count_characters(ctx, inst, "rcx", "r8")?; + abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + Ok(()) +} + +/// Loads the nullable optional `str_word_count()` character list into a pointer/length pair. +/// +/// An omitted or `null` `$characters` argument becomes a zero-length list, which builds the +/// same membership table php-src derives from a `NULL` char list. +fn load_optional_str_word_count_characters( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + ptr_reg: &str, + len_reg: &str, +) -> Result<()> { + let Some(characters) = inst.operands.get(2).copied() else { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + }; + if matches!(ctx.value_php_type(characters)?, PhpType::Void | PhpType::Never) { + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + return Ok(()); + } + load_value_as_string_to_regs(ctx, characters, "str_word_count characters", ptr_reg, len_reg) +} + /// Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper. pub(crate) fn lower_str_pad(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if inst.operands.len() < 2 || inst.operands.len() > 4 { @@ -59,6 +466,7 @@ pub(crate) fn lower_str_pad(ctx: &mut FunctionContext<'_>, inst: &Instruction) - Arch::AArch64 => lower_str_pad_aarch64(ctx, inst)?, Arch::X86_64 => lower_str_pad_x86_64(ctx, inst)?, } + emit_str_pad_argument_guards(ctx, inst.operands.len() >= 4); abi::emit_call_label(ctx.emitter, "__rt_str_pad"); store_if_result(ctx, inst) } diff --git a/src/codegen/lower_inst/builtins/strings/scalar.rs b/src/codegen/lower_inst/builtins/strings/scalar.rs index 7428e6d420..b3496b5538 100644 --- a/src/codegen/lower_inst/builtins/strings/scalar.rs +++ b/src/codegen/lower_inst/builtins/strings/scalar.rs @@ -206,7 +206,11 @@ pub(super) fn load_as_float(ctx: &mut FunctionContext<'_>, value: ValueId, name: } /// Loads a concrete scalar value as an integer runtime argument. -pub(super) fn load_as_int(ctx: &mut FunctionContext<'_>, value: ValueId, name: &str) -> Result<()> { +pub(crate) fn load_as_int( + ctx: &mut FunctionContext<'_>, + value: ValueId, + name: &str, +) -> Result<()> { match ctx.load_value_to_result(value)?.codegen_repr() { PhpType::Int | PhpType::Bool => Ok(()), PhpType::Void | PhpType::Never => { diff --git a/src/codegen/lower_inst/builtins/strings/search.rs b/src/codegen/lower_inst/builtins/strings/search.rs index 40466912c5..182762a734 100644 --- a/src/codegen/lower_inst/builtins/strings/search.rs +++ b/src/codegen/lower_inst/builtins/strings/search.rs @@ -28,18 +28,217 @@ pub(crate) fn lower_str_contains(ctx: &mut FunctionContext<'_>, inst: &Instructi } /// Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed. +/// +/// With two operands this is the plain whole-haystack search. With three, `$offset` is +/// normalized here rather than inside the runtime helper because an offset outside the +/// haystack is a catchable `ValueError` in reference PHP, and only the backend can emit a +/// throw the surrounding `try` will observe. The helper therefore always receives a window +/// that is known to sit inside the haystack, plus the absolute base offset that has to be +/// added back to a successful match. pub(crate) fn lower_string_position( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, runtime_label: &str, + direction: StringPositionDirection, ) -> Result<()> { - load_binary_string_args(ctx, inst, name)?; + if inst.operands.len() == 2 { + load_binary_string_args(ctx, inst, name)?; + abi::emit_call_label(ctx.emitter, runtime_label); + box_search_result(ctx, name); + return store_if_result(ctx, inst); + } + if inst.operands.len() != 3 { + return Err(CodegenIrError::invalid_module(format!( + "{} expected 2 or 3 args, got {}", + name, + inst.operands.len() + ))); + } + load_string_position_args(ctx, inst, name)?; + emit_string_position_offset_guard(ctx, name, direction); + abi::emit_push_reg(ctx.emitter, string_position_base_reg(ctx)); abi::emit_call_label(ctx.emitter, runtime_label); + abi::emit_pop_reg(ctx.emitter, string_position_base_reg(ctx)); + emit_string_position_rebase(ctx, name); box_search_result(ctx, name); store_if_result(ctx, inst) } +/// Returns the scratch register that carries a `strpos()`-family search's base offset. +/// +/// The base is the number of haystack bytes the runtime helper never sees, so it is also +/// the value added back to a match before the result is boxed. It deliberately reuses the +/// register the offset was materialized into, which is the first argument register past +/// the haystack/needle pointer-length pairs on both supported ABIs. +fn string_position_base_reg(ctx: &FunctionContext<'_>) -> &'static str { + match ctx.emitter.target.arch { + Arch::AArch64 => "x5", + Arch::X86_64 => "r8", + } +} + +/// Materializes a three-argument `strpos()`-family call into its runtime ABI registers. +/// +/// Leaves the haystack in the primary string pointer/length pair, the needle in the +/// secondary pair, and the raw (still unnormalized) `$offset` in the scratch base register. +fn load_string_position_args( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, +) -> Result<()> { + let offset = expect_operand(inst, 2)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_string_arg_to_regs(ctx, inst, 0, name, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the haystack pointer and length while the needle is materialized + load_string_arg_to_regs(ctx, inst, 1, name, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the needle pointer and length while the offset is materialized + load_as_int(ctx, offset, name)?; + ctx.emitter.instruction("mov x5, x0"); // park the raw search offset until the haystack length is known + ctx.emitter.instruction("ldp x3, x4, [sp], #16"); // restore the needle into the secondary runtime string argument + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the haystack into the primary runtime string argument + } + Arch::X86_64 => { + load_string_arg_to_regs(ctx, inst, 0, name, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_string_arg_to_regs(ctx, inst, 1, name, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_as_int(ctx, offset, name)?; + ctx.emitter.instruction("mov r8, rax"); // park the raw search offset until the haystack length is known + abi::emit_pop_reg_pair(ctx.emitter, "rdx", "rcx"); // restore the needle into the secondary SysV string argument + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); // restore the haystack into the primary SysV string argument + } + } + Ok(()) +} + +/// Turns a raw `strpos()`-family `$offset` into a searched window plus a base offset. +/// +/// Rejects, with php-src's verbatim `ValueError`, every offset that does not land inside the +/// haystack: `$offset > strlen($haystack)` in both directions, and `-$offset > +/// strlen($haystack)` for a negative one. On success the primary string pair describes the +/// bytes the runtime helper may scan and the base register holds the offset that must be +/// added back to a match. +fn emit_string_position_offset_guard( + ctx: &mut FunctionContext<'_>, + name: &str, + direction: StringPositionDirection, +) { + let non_negative_label = ctx.next_label("strpos_offset_non_negative"); + let bad_label = ctx.next_label("strpos_offset_bad"); + let ok_label = ctx.next_label("strpos_offset_ok"); + let whole_label = ctx.next_label("strpos_offset_whole"); + match (ctx.emitter.target.arch, direction) { + (Arch::AArch64, StringPositionDirection::Forward) => { + ctx.emitter.instruction("cmp x5, #0"); // is the requested offset measured from the haystack end? + ctx.emitter.instruction(&format!("b.ge {}", non_negative_label)); // a non-negative offset is already absolute + ctx.emitter.instruction("add x5, x5, x2"); // resolve a negative offset against the haystack length + ctx.emitter.instruction("cmp x5, #0"); // did the negative offset reach past the haystack start? + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // an offset still inside the haystack is usable + ctx.emitter.instruction(&format!("b {}", bad_label)); // an offset before the haystack start is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp x5, x2"); // compare the absolute offset against the haystack length + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // an offset at or before the haystack end is usable + ctx.emitter.label(&bad_label); + } + (Arch::AArch64, StringPositionDirection::Reverse) => { + ctx.emitter.instruction("cmp x5, #0"); // is the requested offset measured from the haystack end? + ctx.emitter.instruction(&format!("b.ge {}", non_negative_label)); // a non-negative offset starts the right-to-left scan + ctx.emitter.instruction("neg x9, x5"); // take the magnitude of the negative offset + ctx.emitter.instruction("cmp x9, x2"); // did the negative offset reach past the haystack start? + ctx.emitter.instruction(&format!("b.gt {}", bad_label)); // an offset before the haystack start is rejected + ctx.emitter.instruction("cmp x9, x4"); // can a match still overlap the trimmed tail? + ctx.emitter.instruction(&format!("b.lt {}", whole_label)); // a magnitude below the needle length leaves the whole haystack searchable + ctx.emitter.instruction("add x2, x2, x5"); // drop the trailing bytes the negative offset excludes + ctx.emitter.instruction("add x2, x2, x4"); // keep the bytes a match ending on the boundary still needs + ctx.emitter.label(&whole_label); + ctx.emitter.instruction("mov x5, #0"); // a negative offset never slides the haystack, so matches are already absolute + ctx.emitter.instruction(&format!("b {}", ok_label)); // the negative-offset window is ready for the runtime helper + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp x5, x2"); // compare the absolute offset against the haystack length + ctx.emitter.instruction(&format!("b.gt {}", bad_label)); // an offset past the haystack end is rejected + ctx.emitter.instruction("add x1, x1, x5"); // slide the haystack pointer to the first searchable byte + ctx.emitter.instruction("sub x2, x2, x5"); // shrink the haystack length to the searched window + ctx.emitter.instruction(&format!("b {}", ok_label)); // the non-negative-offset window is ready for the runtime helper + ctx.emitter.label(&bad_label); + } + (Arch::X86_64, StringPositionDirection::Forward) => { + ctx.emitter.instruction("cmp r8, 0"); // is the requested offset measured from the haystack end? + ctx.emitter.instruction(&format!("jge {}", non_negative_label)); // a non-negative offset is already absolute + ctx.emitter.instruction("add r8, rsi"); // resolve a negative offset against the haystack length + ctx.emitter.instruction("cmp r8, 0"); // did the negative offset reach past the haystack start? + ctx.emitter.instruction(&format!("jge {}", ok_label)); // an offset still inside the haystack is usable + ctx.emitter.instruction(&format!("jmp {}", bad_label)); // an offset before the haystack start is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp r8, rsi"); // compare the absolute offset against the haystack length + ctx.emitter.instruction(&format!("jle {}", ok_label)); // an offset at or before the haystack end is usable + ctx.emitter.label(&bad_label); + } + (Arch::X86_64, StringPositionDirection::Reverse) => { + ctx.emitter.instruction("cmp r8, 0"); // is the requested offset measured from the haystack end? + ctx.emitter.instruction(&format!("jge {}", non_negative_label)); // a non-negative offset starts the right-to-left scan + ctx.emitter.instruction("mov r10, r8"); // copy the negative offset before taking its magnitude + ctx.emitter.instruction("neg r10"); // take the magnitude of the negative offset + ctx.emitter.instruction("cmp r10, rsi"); // did the negative offset reach past the haystack start? + ctx.emitter.instruction(&format!("jg {}", bad_label)); // an offset before the haystack start is rejected + ctx.emitter.instruction("cmp r10, rcx"); // can a match still overlap the trimmed tail? + ctx.emitter.instruction(&format!("jl {}", whole_label)); // a magnitude below the needle length leaves the whole haystack searchable + ctx.emitter.instruction("add rsi, r8"); // drop the trailing bytes the negative offset excludes + ctx.emitter.instruction("add rsi, rcx"); // keep the bytes a match ending on the boundary still needs + ctx.emitter.label(&whole_label); + ctx.emitter.instruction("xor r8d, r8d"); // a negative offset never slides the haystack, so matches are already absolute + ctx.emitter.instruction(&format!("jmp {}", ok_label)); // the negative-offset window is ready for the runtime helper + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp r8, rsi"); // compare the absolute offset against the haystack length + ctx.emitter.instruction(&format!("jg {}", bad_label)); // an offset past the haystack end is rejected + ctx.emitter.instruction("add rdi, r8"); // slide the haystack pointer to the first searchable byte + ctx.emitter.instruction("sub rsi, r8"); // shrink the haystack length to the searched window + ctx.emitter.instruction(&format!("jmp {}", ok_label)); // the non-negative-offset window is ready for the runtime helper + ctx.emitter.label(&bad_label); + } + } + super::super::exceptions::emit_value_error( + ctx, + &format!("{}{}", name, STRING_POSITION_OFFSET_OUT_OF_RANGE_SUFFIX), + ); + ctx.emitter.label(&ok_label); + if direction == StringPositionDirection::Forward { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("add x1, x1, x5"); // slide the haystack pointer to the first searchable byte + ctx.emitter.instruction("sub x2, x2, x5"); // shrink the haystack length to the searched window + } + Arch::X86_64 => { + ctx.emitter.instruction("add rdi, r8"); // slide the haystack pointer to the first searchable byte + ctx.emitter.instruction("sub rsi, r8"); // shrink the haystack length to the searched window + } + } + } +} + +/// Turns a window-relative `strpos()`-family match back into a haystack-absolute offset. +/// +/// The runtime helper only ever saw the searched window, so a found position has to gain the +/// base offset again. The not-found sentinel is signed and must survive untouched, which is +/// why the addition is branched over instead of applied unconditionally. +fn emit_string_position_rebase(ctx: &mut FunctionContext<'_>, name: &str) { + let done_label = ctx.next_label(&format!("{}_rebase_done", name)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #0"); // distinguish a window-relative match from the not-found sentinel + ctx.emitter.instruction(&format!("b.lt {}", done_label)); // leave the not-found sentinel alone + ctx.emitter.instruction("add x0, x0, x5"); // restore the haystack-absolute offset of the match + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 0"); // distinguish a window-relative match from the not-found sentinel + ctx.emitter.instruction(&format!("jl {}", done_label)); // leave the not-found sentinel alone + ctx.emitter.instruction("add rax, r8"); // restore the haystack-absolute offset of the match + } + } + ctx.emitter.label(&done_label); +} + /// Lowers `substr(string, offset, length?)` with target-local pointer arithmetic. pub(crate) fn lower_substr(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if inst.operands.len() < 2 || inst.operands.len() > 3 { @@ -73,6 +272,240 @@ pub(crate) fn lower_substr_replace(ctx: &mut FunctionContext<'_>, inst: &Instruc store_if_result(ctx, inst) } +/// Lowers `substr_count(haystack, needle, offset?, length?)` through the shared counter. +/// +/// `$offset`/`$length` are normalized here rather than inside `__rt_substr_count` because +/// every out-of-range value is a catchable `ValueError` in reference PHP, and only the +/// backend can emit a throw the surrounding `try` will see. The helper therefore receives a +/// window that is already known to sit inside the subject. +pub(crate) fn lower_substr_count(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + if inst.operands.len() < 2 || inst.operands.len() > 4 { + return Err(CodegenIrError::invalid_module(format!( + "substr_count expected 2 to 4 args, got {}", + inst.operands.len() + ))); + } + let has_length = substr_count_has_length(ctx, inst)?; + match ctx.emitter.target.arch { + Arch::AArch64 => lower_substr_count_aarch64(ctx, inst, has_length)?, + Arch::X86_64 => lower_substr_count_x86_64(ctx, inst, has_length)?, + } + emit_substr_count_argument_guards(ctx, has_length); + abi::emit_call_label(ctx.emitter, "__rt_substr_count"); + store_if_result(ctx, inst) +} + +/// Reports whether `substr_count()` was given a `$length` that actually bounds the window. +/// +/// PHP's default is `null`, meaning "to the end of the subject", and an explicitly written +/// `null` behaves identically. A statically-null operand (checker type `Void`/`Never`) is +/// therefore treated exactly like an omitted argument instead of being coerced to `0`, which +/// would have counted matches inside an empty window. +fn substr_count_has_length(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result { + let Some(length) = inst.operands.get(3) else { + return Ok(false); + }; + Ok(!matches!( + ctx.value_php_type(*length)?.codegen_repr(), + PhpType::Void | PhpType::Never + )) +} + +/// Materializes AArch64 `substr_count()` arguments into the counter's ABI registers. +/// +/// Leaves `x1`/`x2` = subject, `x3`/`x4` = needle, `x5` = raw `$offset`, and `x6` = raw +/// `$length` when one was supplied. The guards that follow turn the subject plus the raw +/// offset/length pair into the window the runtime helper scans. +fn lower_substr_count_aarch64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + has_length: bool, +) -> Result<()> { + let haystack = expect_operand(inst, 0)?; + let needle = expect_operand(inst, 1)?; + load_value_as_string_to_regs(ctx, haystack, "substr_count", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject string while materializing the remaining arguments + load_value_as_string_to_regs(ctx, needle, "substr_count", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the needle string while materializing the window bounds + if inst.operands.len() >= 3 { + let offset = expect_operand(inst, 2)?; + load_as_int(ctx, offset, "substr_count offset")?; + } else { + abi::emit_load_int_immediate(ctx.emitter, "x0", 0); + } + abi::emit_push_reg(ctx.emitter, "x0"); + if has_length { + let length = expect_operand(inst, 3)?; + load_as_int(ctx, length, "substr_count length")?; + } else { + abi::emit_load_int_immediate(ctx.emitter, "x0", 0); + } + ctx.emitter.instruction("mov x6, x0"); // park the raw window length until the subject length is known + abi::emit_pop_reg(ctx.emitter, "x5"); + ctx.emitter.instruction("ldp x3, x4, [sp], #16"); // restore the needle into the secondary runtime string argument + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the primary runtime string argument + Ok(()) +} + +/// Materializes x86_64 `substr_count()` arguments into the counter's ABI registers. +/// +/// Leaves `rdi`/`rsi` = subject, `rdx`/`rcx` = needle, `r8` = raw `$offset`, and `r9` = raw +/// `$length` when one was supplied, mirroring the AArch64 emitter's register roles. +fn lower_substr_count_x86_64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + has_length: bool, +) -> Result<()> { + let haystack = expect_operand(inst, 0)?; + let needle = expect_operand(inst, 1)?; + load_value_as_string_to_regs(ctx, haystack, "substr_count", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_value_as_string_to_regs(ctx, needle, "substr_count", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + if inst.operands.len() >= 3 { + let offset = expect_operand(inst, 2)?; + load_as_int(ctx, offset, "substr_count offset")?; + } else { + abi::emit_load_int_immediate(ctx.emitter, "rax", 0); + } + abi::emit_push_reg(ctx.emitter, "rax"); + if has_length { + let length = expect_operand(inst, 3)?; + load_as_int(ctx, length, "substr_count length")?; + } else { + abi::emit_load_int_immediate(ctx.emitter, "rax", 0); + } + ctx.emitter.instruction("mov r9, rax"); // park the raw window length until the subject length is known + abi::emit_pop_reg(ctx.emitter, "r8"); + abi::emit_pop_reg_pair(ctx.emitter, "rdx", "rcx"); + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); + Ok(()) +} + +/// Validates and normalizes the `substr_count()` window, raising PHP's `ValueError`s. +/// +/// php-src checks in exactly this order: the empty `$needle` first, then `$offset` (negative +/// values count back from the subject end and must not underflow it, positive values must not +/// pass its end), then `$length` (negative values are measured back from the subject end, so +/// they are added to the bytes remaining after `$offset`, and neither direction may leave the +/// subject). Afterwards the subject registers hold the window the counter scans. +fn emit_substr_count_argument_guards(ctx: &mut FunctionContext<'_>, has_length: bool) { + emit_substr_count_needle_guard(ctx); + emit_substr_count_offset_guard(ctx); + emit_substr_count_length_guard(ctx, has_length); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("add x1, x1, x5"); // slide the subject pointer to the start of the counted window + ctx.emitter.instruction("mov x2, x6"); // pass the resolved window length to the counter + } + Arch::X86_64 => { + ctx.emitter.instruction("add rdi, r8"); // slide the subject pointer to the start of the counted window + ctx.emitter.instruction("mov rsi, r9"); // pass the resolved window length to the counter + } + } +} + +/// Rejects the empty `substr_count()` needle reference PHP refuses to count. +fn emit_substr_count_needle_guard(ctx: &mut FunctionContext<'_>) { + let ok_label = ctx.next_label("substr_count_needle_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbnz x4, {}", ok_label)); // a non-empty needle can be counted + } + Arch::X86_64 => { + ctx.emitter.instruction("test rcx, rcx"); // is the needle zero-length? + ctx.emitter.instruction(&format!("jnz {}", ok_label)); // a non-empty needle can be counted + } + } + super::super::exceptions::emit_value_error(ctx, SUBSTR_COUNT_EMPTY_NEEDLE_MESSAGE); + ctx.emitter.label(&ok_label); +} + +/// Normalizes `substr_count()`'s `$offset` and rejects one that leaves the subject. +fn emit_substr_count_offset_guard(ctx: &mut FunctionContext<'_>) { + let non_negative_label = ctx.next_label("substr_count_offset_non_negative"); + let bad_label = ctx.next_label("substr_count_offset_bad"); + let ok_label = ctx.next_label("substr_count_offset_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x5, #0"); // is the requested offset measured from the subject end? + ctx.emitter.instruction(&format!("b.ge {}", non_negative_label)); // a non-negative offset is already absolute + ctx.emitter.instruction("add x5, x5, x2"); // resolve a negative offset against the subject length + ctx.emitter.instruction("cmp x5, #0"); // did the negative offset reach past the subject start? + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // an offset still inside the subject is usable + ctx.emitter.instruction(&format!("b {}", bad_label)); // an offset before the subject start is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp x5, x2"); // compare the absolute offset against the subject length + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // an offset at or before the subject end is usable + ctx.emitter.label(&bad_label); + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp r8, 0"); // is the requested offset measured from the subject end? + ctx.emitter.instruction(&format!("jge {}", non_negative_label)); // a non-negative offset is already absolute + ctx.emitter.instruction("add r8, rsi"); // resolve a negative offset against the subject length + ctx.emitter.instruction("cmp r8, 0"); // did the negative offset reach past the subject start? + ctx.emitter.instruction(&format!("jge {}", ok_label)); // an offset still inside the subject is usable + ctx.emitter.instruction(&format!("jmp {}", bad_label)); // an offset before the subject start is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp r8, rsi"); // compare the absolute offset against the subject length + ctx.emitter.instruction(&format!("jle {}", ok_label)); // an offset at or before the subject end is usable + ctx.emitter.label(&bad_label); + } + } + super::super::exceptions::emit_value_error(ctx, SUBSTR_COUNT_OFFSET_OUT_OF_RANGE_MESSAGE); + ctx.emitter.label(&ok_label); +} + +/// Resolves `substr_count()`'s `$length` into a window size and rejects out-of-subject values. +/// +/// With no explicit `$length` the window simply runs to the subject end. Otherwise a negative +/// length is measured back from that end, which is why it is added to the remaining byte count +/// rather than to the offset. +fn emit_substr_count_length_guard(ctx: &mut FunctionContext<'_>, has_length: bool) { + let non_negative_label = ctx.next_label("substr_count_length_non_negative"); + let bad_label = ctx.next_label("substr_count_length_bad"); + let ok_label = ctx.next_label("substr_count_length_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("sub x9, x2, x5"); // compute the bytes remaining after the resolved offset + if !has_length { + ctx.emitter.instruction("mov x6, x9"); // an omitted or null length runs to the subject end + return; + } + ctx.emitter.instruction("cmp x6, #0"); // is the requested length measured back from the subject end? + ctx.emitter.instruction(&format!("b.ge {}", non_negative_label)); // a non-negative length is already a window size + ctx.emitter.instruction("add x6, x6, x9"); // resolve a negative length against the remaining bytes + ctx.emitter.instruction("cmp x6, #0"); // did the negative length cross back before the offset? + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // a window that still has a non-negative size is usable + ctx.emitter.instruction(&format!("b {}", bad_label)); // a window that ends before it starts is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp x6, x9"); // compare the requested window against the remaining bytes + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // a window inside the subject is usable + ctx.emitter.label(&bad_label); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov r10, rsi"); // copy the subject length before deriving the remaining bytes + ctx.emitter.instruction("sub r10, r8"); // compute the bytes remaining after the resolved offset + if !has_length { + ctx.emitter.instruction("mov r9, r10"); // an omitted or null length runs to the subject end + return; + } + ctx.emitter.instruction("cmp r9, 0"); // is the requested length measured back from the subject end? + ctx.emitter.instruction(&format!("jge {}", non_negative_label)); // a non-negative length is already a window size + ctx.emitter.instruction("add r9, r10"); // resolve a negative length against the remaining bytes + ctx.emitter.instruction("cmp r9, 0"); // did the negative length cross back before the offset? + ctx.emitter.instruction(&format!("jge {}", ok_label)); // a window that still has a non-negative size is usable + ctx.emitter.instruction(&format!("jmp {}", bad_label)); // a window that ends before it starts is rejected + ctx.emitter.label(&non_negative_label); + ctx.emitter.instruction("cmp r9, r10"); // compare the requested window against the remaining bytes + ctx.emitter.instruction(&format!("jle {}", ok_label)); // a window inside the subject is usable + ctx.emitter.label(&bad_label); + } + } + super::super::exceptions::emit_value_error(ctx, SUBSTR_COUNT_LENGTH_OUT_OF_RANGE_MESSAGE); + ctx.emitter.label(&ok_label); +} + /// Lowers `str_repeat(string, times)` through the shared runtime helper. pub(crate) fn lower_str_repeat(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if inst.operands.len() != 2 { @@ -85,6 +518,18 @@ pub(crate) fn lower_str_repeat(ctx: &mut FunctionContext<'_>, inst: &Instruction Arch::AArch64 => lower_str_repeat_aarch64(ctx, inst)?, Arch::X86_64 => lower_str_repeat_x86_64(ctx, inst)?, } + // `__rt_str_repeat` still carries its own negative-count fatal as a backstop, but that + // fatal is not catchable. Reference PHP raises a ValueError here, so screen the count + // before the helper ever sees it. + let times_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedAtLeast(times_reg, 0), + STR_REPEAT_NEGATIVE_TIMES_MESSAGE, + ); abi::emit_call_label(ctx.emitter, "__rt_str_repeat"); store_if_result(ctx, inst) } diff --git a/src/codegen/lower_inst/builtins/strings/simple.rs b/src/codegen/lower_inst/builtins/strings/simple.rs index 167d69e224..a205e08678 100644 --- a/src/codegen/lower_inst/builtins/strings/simple.rs +++ b/src/codegen/lower_inst/builtins/strings/simple.rs @@ -9,17 +9,6 @@ use super::*; -/// Lowers a one-argument string builtin that directly delegates to a runtime helper. -pub(crate) fn lower_unary_string_runtime( - ctx: &mut FunctionContext<'_>, - inst: &Instruction, - name: &str, - runtime_label: &str, -) -> Result<()> { - load_single_string_arg(ctx, inst, name)?; - abi::emit_call_label(ctx.emitter, runtime_label); - store_if_result(ctx, inst) -} /// Lowers `htmlspecialchars()` / `htmlentities()` — escapes the subject string (operand 0). /// `name` is the calling builtin's PHP name, used in argument-coercion diagnostics. The diff --git a/src/codegen/lower_inst/builtins/strings/split.rs b/src/codegen/lower_inst/builtins/strings/split.rs index 3b771e7d99..dcbdad3177 100644 --- a/src/codegen/lower_inst/builtins/strings/split.rs +++ b/src/codegen/lower_inst/builtins/strings/split.rs @@ -50,17 +50,203 @@ impl SplitStringTempCleanups { } } /// Lowers `explode(delimiter, string)` into the shared string-array splitter helper. +/// Lowers `dechex()`/`decbin()`/`decoct()` through the shared unsigned base renderer. +/// +/// The three builtins differ only in the constant base handed to `__rt_dec_to_base`, which +/// reads its input as unsigned — that is what makes `dechex(-1)` render `"ffffffffffffffff"` +/// instead of a signed value. +pub(crate) fn lower_dec_to_base( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, + base: i64, +) -> Result<()> { + if inst.operands.len() != 1 { + return Err(CodegenIrError::invalid_module(format!( + "{} expected 1 arg, got {}", + name, + inst.operands.len() + ))); + } + load_as_int(ctx, expect_operand(inst, 0)?, name)?; + let base_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + abi::emit_load_int_immediate(ctx.emitter, base_reg, base); + abi::emit_call_label(ctx.emitter, "__rt_dec_to_base"); + store_if_result(ctx, inst) +} + +/// Lowers `hexdec()`/`bindec()`/`octdec()` through the shared base-digit parser. +/// +/// The three builtins differ only in the constant base handed to `__rt_base_to_number`. +/// That helper reports whether its answer stayed an integer or widened to a float, and this +/// lowering boxes the selected arm into the `int|float` union's `Mixed` representation. +pub(crate) fn lower_base_to_number( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, + base: i64, +) -> Result<()> { + if inst.operands.len() != 1 { + return Err(CodegenIrError::invalid_module(format!( + "{} expected 1 arg, got {}", + name, + inst.operands.len() + ))); + } + let subject = expect_operand(inst, 0)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + load_value_as_string_to_regs(ctx, subject, name, "x1", "x2")?; + abi::emit_load_int_immediate(ctx.emitter, "x3", base); + } + Arch::X86_64 => { + load_value_as_string_to_regs(ctx, subject, name, "rax", "rdx")?; + ctx.emitter.instruction("mov rdi, rax"); // pass the subject pointer as the first SysV argument + ctx.emitter.instruction("mov rsi, rdx"); // pass the subject length before the base overwrites rdx + abi::emit_load_int_immediate(ctx.emitter, "rdx", base); + } + } + abi::emit_call_label(ctx.emitter, "__rt_base_to_number"); + box_base_to_number_result(ctx, name); + store_if_result(ctx, inst) +} + +/// Boxes `__rt_base_to_number`'s integer-or-float answer as PHP's `int|float` union. +/// +/// The helper reports its arm in the integer result register: zero selects the integer +/// payload it left alongside it, one selects the float payload in the float result register. +fn box_base_to_number_result(ctx: &mut FunctionContext<'_>, name: &str) { + let float_label = ctx.next_label(&format!("{}_float", name)); + let done_label = ctx.next_label(&format!("{}_done", name)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbnz x0, {}", float_label)); // a widened result is boxed from the float register instead + ctx.emitter.instruction("mov x2, xzr"); // integer mixed payloads do not use a high word + ctx.emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip float boxing after producing the integer result + ctx.emitter.label(&float_label); + ctx.emitter.instruction("fmov x1, d0"); // move the widened float bits into the mixed helper payload register + ctx.emitter.instruction("mov x2, xzr"); // float mixed payloads do not use a high word + ctx.emitter.instruction("mov x0, #2"); // runtime tag 2 = float + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + ctx.emitter.label(&done_label); + } + Arch::X86_64 => { + ctx.emitter.instruction("test rax, rax"); // did the parse stay inside PHP's integer range? + ctx.emitter.instruction(&format!("jnz {}", float_label)); // a widened result is boxed from the float register instead + ctx.emitter.instruction("mov rdi, rdx"); // move the parsed integer into the mixed helper payload register + ctx.emitter.instruction("xor esi, esi"); // integer mixed payloads do not use a high word + ctx.emitter.instruction("xor eax, eax"); // runtime tag 0 = integer + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip float boxing after producing the integer result + ctx.emitter.label(&float_label); + ctx.emitter.instruction("movq rdi, xmm0"); // move the widened float bits into the mixed helper payload register + ctx.emitter.instruction("xor esi, esi"); // float mixed payloads do not use a high word + ctx.emitter.instruction("mov eax, 2"); // runtime tag 2 = float + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + ctx.emitter.label(&done_label); + } + } +} + +/// Lowers `strncmp()`/`strncasecmp()`, which compare only the first `$length` bytes. +/// +/// `$length` is screened before the helper runs because reference PHP raises a catchable +/// `ValueError` for a negative value; the runtime helpers therefore treat their bound as +/// unsigned. `name` selects the php-src wording of that diagnostic. +pub(crate) fn lower_length_limited_compare( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, + runtime_label: &str, +) -> Result<()> { + if inst.operands.len() != 3 { + return Err(CodegenIrError::invalid_module(format!( + "{} expected 3 args, got {}", + name, + inst.operands.len() + ))); + } + let length_reg = match ctx.emitter.target.arch { + Arch::AArch64 => { + load_value_as_string_to_regs(ctx, expect_operand(inst, 0)?, name, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the first string while materializing the remaining arguments + load_value_as_string_to_regs(ctx, expect_operand(inst, 1)?, name, "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the second string while materializing the compare length + load_as_int(ctx, expect_operand(inst, 2)?, name)?; + ctx.emitter.instruction("mov x5, x0"); // pass the requested compare length as the fifth runtime argument + ctx.emitter.instruction("ldp x3, x4, [sp], #16"); // restore the second string into the secondary runtime string argument + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the first string into the primary runtime string argument + "x5" + } + Arch::X86_64 => { + load_value_as_string_to_regs(ctx, expect_operand(inst, 0)?, name, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_value_as_string_to_regs(ctx, expect_operand(inst, 1)?, name, "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + load_as_int(ctx, expect_operand(inst, 2)?, name)?; + ctx.emitter.instruction("mov r8, rax"); // pass the requested compare length as the fifth SysV argument + abi::emit_pop_reg_pair(ctx.emitter, "rdx", "rcx"); + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); + "r8" + } + }; + let message = if name == "strncasecmp" { + STRNCASECMP_NEGATIVE_LENGTH_MESSAGE + } else { + STRNCMP_NEGATIVE_LENGTH_MESSAGE + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedAtLeast(length_reg, 0), + message, + ); + abi::emit_call_label(ctx.emitter, runtime_label); + store_if_result(ctx, inst) +} + +/// Lowers `explode(separator, string, limit?)` into the shared string-array splitter helper. pub(crate) fn lower_explode(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let cleanups = plan_split_string_temp_cleanups(ctx, inst)?; if !cleanups.is_empty() { abi::emit_reserve_temporary_stack(ctx.emitter, cleanups.bytes); } load_split_pair_args(ctx, inst, "explode", &cleanups)?; + emit_explode_separator_guard(ctx, &cleanups); abi::emit_call_label(ctx.emitter, "__rt_explode"); emit_split_string_temp_cleanups(ctx, &cleanups); store_if_result(ctx, inst) } +/// Rejects the empty `explode()` separator reference PHP refuses to split on. +/// +/// A zero-length separator matches at every position, so the pre-guard splitter advanced its +/// cursor by zero bytes and pushed empty segments until the heap was exhausted. The guard +/// runs after argument materialization, so any owned string temporaries are released on the +/// throwing path before the unwinder takes over. +fn emit_explode_separator_guard( + ctx: &mut FunctionContext<'_>, + cleanups: &SplitStringTempCleanups, +) { + let ok_label = ctx.next_label("explode_separator_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbnz x2, {}", ok_label)); // a non-empty separator can split the subject + } + Arch::X86_64 => { + ctx.emitter.instruction("test rdx, rdx"); // is the separator zero-length? + ctx.emitter.instruction(&format!("jnz {}", ok_label)); // a non-empty separator can split the subject + } + } + emit_split_string_temp_cleanups(ctx, cleanups); + super::super::exceptions::emit_value_error(ctx, EXPLODE_EMPTY_SEPARATOR_MESSAGE); + ctx.emitter.label(&ok_label); +} + /// Lowers `sscanf(string, format)` into the shared scanner helper. pub(crate) fn lower_sscanf(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if inst.operands.len() < 2 { @@ -86,44 +272,63 @@ pub(crate) fn lower_str_split(ctx: &mut FunctionContext<'_>, inst: &Instruction) Arch::AArch64 => lower_str_split_aarch64(ctx, inst)?, Arch::X86_64 => lower_str_split_x86_64(ctx, inst)?, } + // `__rt_str_split` advances its cursor by the chunk length, so a zero length spins + // forever pushing empty chunks until the heap is exhausted and a negative one walks + // the cursor backwards off the string. Reference PHP rejects both up front. + let length_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x3", + Arch::X86_64 => "rdi", + }; + super::super::exceptions::emit_value_error_unless( + ctx, + super::super::exceptions::ValueGuard::SignedAtLeast(length_reg, 1), + STR_SPLIT_NON_POSITIVE_LENGTH_MESSAGE, + ); abi::emit_call_label(ctx.emitter, "__rt_str_split"); store_if_result(ctx, inst) } -/// Lowers `implode(glue, array)` by selecting the string or integer array helper. +/// Lowers `implode(glue, array)` / `join(array)` by selecting the array-element helper. +/// +/// The typed target is shared by both PHP names, so the operand roles are derived from the +/// argument count rather than the source spelling: a single operand is the ARRAY and the glue +/// is the empty string (`join(["a","b"]) === "ab"`), while two operands keep the ordinary +/// `(glue, array)` order. The reversed PHP 7 order was removed in PHP 8.0 and is not accepted. pub(crate) fn lower_implode(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - if inst.operands.len() != 2 { + if inst.operands.is_empty() || inst.operands.len() > 2 { return Err(CodegenIrError::invalid_module(format!( - "implode expected 2 args, got {}", + "implode expected 1 or 2 args, got {}", inst.operands.len() ))); } - let runtime_label = implode_runtime_label(ctx, inst)?; + let array_index = inst.operands.len() - 1; + let runtime_label = implode_runtime_label(ctx, inst, array_index)?; match ctx.emitter.target.arch { - Arch::AArch64 => lower_implode_aarch64(ctx, inst)?, - Arch::X86_64 => lower_implode_x86_64(ctx, inst)?, + Arch::AArch64 => lower_implode_aarch64(ctx, inst, array_index)?, + Arch::X86_64 => lower_implode_x86_64(ctx, inst, array_index)?, } abi::emit_call_label(ctx.emitter, runtime_label); store_if_result(ctx, inst) } -/// Materializes delimiter/payload string pairs for split-style array helpers. +/// Materializes delimiter/payload string pairs plus the optional `$limit` for `explode()`. pub(super) fn load_split_pair_args( ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str, cleanups: &SplitStringTempCleanups, ) -> Result<()> { - if inst.operands.len() != 2 { + if inst.operands.len() < 2 || inst.operands.len() > 3 { return Err(CodegenIrError::invalid_module(format!( - "{} expected 2 args, got {}", + "{} expected 2 or 3 args, got {}", name, inst.operands.len() ))); } match ctx.emitter.target.arch { - Arch::AArch64 => load_split_pair_args_aarch64(ctx, inst, name, cleanups), - Arch::X86_64 => load_split_pair_args_x86_64(ctx, inst, name, cleanups), + Arch::AArch64 => load_split_pair_args_aarch64(ctx, inst, name, cleanups)?, + Arch::X86_64 => load_split_pair_args_x86_64(ctx, inst, name, cleanups)?, } + load_split_limit_arg(ctx, inst, name) } /// Materializes AArch64 delimiter and subject strings for `explode()`. @@ -329,8 +534,15 @@ pub(super) fn materialize_str_split_length_x86_64( } /// Returns the runtime helper label required for an `implode()` array operand. -pub(super) fn implode_runtime_label(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result<&'static str> { - let array = expect_operand(inst, 1)?; +/// +/// `array_index` is 1 for the ordinary `(glue, array)` call and 0 for the single-argument +/// `join($array)` form, whose only operand is the array itself. +pub(super) fn implode_runtime_label( + ctx: &FunctionContext<'_>, + inst: &Instruction, + array_index: usize, +) -> Result<&'static str> { + let array = expect_operand(inst, array_index)?; match ctx.value_php_type(array)? { PhpType::Array(elem_ty) => match elem_ty.codegen_repr() { // PHP stringifies bool elements as "1"/"" — NOT as the "1"/"0" that @@ -338,7 +550,13 @@ pub(super) fn implode_runtime_label(ctx: &FunctionContext<'_>, inst: &Instructio // own renderer. `PhpType::False` reaches this arm as `Bool` through `codegen_repr`. PhpType::Bool => Ok("__rt_implode_bool"), PhpType::Int => Ok("__rt_implode_int"), - PhpType::Str | PhpType::Mixed | PhpType::Never => Ok("__rt_implode"), + // An empty array literal carries an uninhabited element type (`Never`, or + // `Void` once it has gone through `codegen_repr`). Neither renderer can ever + // dereference an element, so the generic string helper is the safe choice and + // keeps `implode("", [])` / `join([])` from being rejected at lowering time. + PhpType::Str | PhpType::Mixed | PhpType::Never | PhpType::Void => { + Ok("__rt_implode") + } other => Err(CodegenIrError::unsupported(format!( "implode array element PHP type {:?}", other @@ -353,10 +571,23 @@ pub(super) fn implode_runtime_label(ctx: &FunctionContext<'_>, inst: &Instructio } /// Materializes AArch64 glue and array arguments for `implode()`. -pub(super) fn lower_implode_aarch64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - let glue = expect_string_operand(ctx, inst, 0, "implode")?; - let array = expect_operand(inst, 1)?; - ctx.load_string_value_to_regs(glue, "x1", "x2")?; +/// +/// `array_index` is 0 for the single-argument `join($array)` form, which joins with an empty +/// separator, and 1 for the ordinary `(glue, array)` call. +pub(super) fn lower_implode_aarch64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + array_index: usize, +) -> Result<()> { + let array = expect_operand(inst, array_index)?; + if array_index == 0 { + let (label, _) = ctx.data.add_string(b""); + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", 0); + } else { + let glue = expect_operand(inst, 0)?; + load_value_as_string_to_regs(ctx, glue, "implode", "x1", "x2")?; + } ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the glue string while materializing the array argument load_implode_array_aarch64(ctx, array)?; ctx.emitter.instruction("mov x3, x0"); // pass the indexed array pointer as the third implode argument @@ -365,10 +596,23 @@ pub(super) fn lower_implode_aarch64(ctx: &mut FunctionContext<'_>, inst: &Instru } /// Materializes x86_64 glue and array arguments for `implode()`. -pub(super) fn lower_implode_x86_64(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - let glue = expect_string_operand(ctx, inst, 0, "implode")?; - let array = expect_operand(inst, 1)?; - ctx.load_string_value_to_regs(glue, "rax", "rdx")?; +/// +/// `array_index` follows the same convention as the AArch64 emitter: 0 selects the +/// single-argument `join($array)` form with an empty separator, 1 the `(glue, array)` call. +pub(super) fn lower_implode_x86_64( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + array_index: usize, +) -> Result<()> { + let array = expect_operand(inst, array_index)?; + if array_index == 0 { + let (label, _) = ctx.data.add_string(b""); + abi::emit_symbol_address(ctx.emitter, "rax", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", 0); + } else { + let glue = expect_operand(inst, 0)?; + load_value_as_string_to_regs(ctx, glue, "implode", "rax", "rdx")?; + } abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); load_implode_array_x86_64(ctx, array)?; ctx.emitter.instruction("mov rdx, rax"); // pass the indexed array pointer as the third implode argument diff --git a/src/codegen/lower_inst/builtins/types.rs b/src/codegen/lower_inst/builtins/types.rs index 1f558ee90c..7093214177 100644 --- a/src/codegen/lower_inst/builtins/types.rs +++ b/src/codegen/lower_inst/builtins/types.rs @@ -22,6 +22,82 @@ use super::super::super::context::FunctionContext; use super::super::predicates; use super::{expect_operand, load_value_to_first_int_arg, store_if_result}; +/// Lowers `intval($value, $base)`, PHP's two-argument integer conversion. +/// +/// Reference PHP honors `$base` only when `$value` is a string, so the subject's checker type +/// picks the path: a known string goes straight to `__rt_str_to_int_base`, a boxed `Mixed` +/// goes to `__rt_mixed_intval_base` (which repeats that test at run time against the cell's +/// tag), and every other scalar keeps the ordinary integer cast with the base discarded — +/// `intval(42.9, 8) === 42`, not `34`. +pub(crate) fn lower_intval_base(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count(inst, "intval", 2)?; + let value = expect_operand(inst, 0)?; + let base = expect_operand(inst, 1)?; + match ctx.value_php_type(value)?.codegen_repr() { + PhpType::Str => lower_intval_base_from_string(ctx, value, base)?, + PhpType::Mixed => lower_intval_base_from_mixed(ctx, value, base)?, + _ => super::strings::load_as_int(ctx, value, "intval")?, + } + store_if_result(ctx, inst) +} + +/// Materializes a known-string `intval()` subject and parses it in the requested base. +/// +/// The subject is staged first because materializing `$base` may itself need the result +/// register, and the string pair is restored only after the base has reached its own +/// argument register. +fn lower_intval_base_from_string( + ctx: &mut FunctionContext<'_>, + value: ValueId, + base: ValueId, +) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + super::strings::load_value_as_string_to_regs(ctx, value, "intval", "x1", "x2")?; + ctx.emitter.instruction("stp x1, x2, [sp, #-16]!"); // preserve the subject string while the base is materialized + super::strings::load_as_int(ctx, base, "intval base")?; + ctx.emitter.instruction("mov x3, x0"); // pass the requested base as the parser's third argument + ctx.emitter.instruction("ldp x1, x2, [sp], #16"); // restore the subject into the parser's string argument pair + } + Arch::X86_64 => { + super::strings::load_value_as_string_to_regs(ctx, value, "intval", "rax", "rdx")?; + abi::emit_push_reg_pair(ctx.emitter, "rax", "rdx"); + super::strings::load_as_int(ctx, base, "intval base")?; + ctx.emitter.instruction("mov r8, rax"); // park the requested base while the subject is restored + abi::emit_pop_reg_pair(ctx.emitter, "rdi", "rsi"); // restore the subject into the parser's SysV string arguments + ctx.emitter.instruction("mov rdx, r8"); // pass the requested base as the parser's third argument + } + } + abi::emit_call_label(ctx.emitter, "__rt_str_to_int_base"); + Ok(()) +} + +/// Materializes a boxed `Mixed` `intval()` subject and defers the string test to run time. +/// +/// The cell pointer stays in the canonical integer result register, which is exactly where +/// `__rt_mixed_intval_base` and the `__rt_mixed_cast_int` it falls back to expect it. +fn lower_intval_base_from_mixed( + ctx: &mut FunctionContext<'_>, + value: ValueId, + base: ValueId, +) -> Result<()> { + let cell_reg = abi::int_result_reg(ctx.emitter); + ctx.load_value_to_result(value)?; + abi::emit_push_reg(ctx.emitter, cell_reg); + super::strings::load_as_int(ctx, base, "intval base")?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the requested base as the helper's second argument + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the requested base as the helper's second argument + } + } + abi::emit_pop_reg(ctx.emitter, cell_reg); + abi::emit_call_label(ctx.emitter, "__rt_mixed_intval_base"); + Ok(()) +} + /// Lowers `settype($local, "type")` by mutating the resolved local slot and returning true. pub(crate) fn lower_settype(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "settype", 2)?; @@ -46,12 +122,31 @@ pub(crate) fn lower_class_alias(ctx: &mut FunctionContext<'_>, inst: &Instructio } /// Rejects `unset()` calls that were not converted into direct EIR unbind operations. +/// +/// Reaching this lowering means `crate::ir_lower::expr` could not turn the target +/// into a slot clear, a hash/array removal, an `offsetUnset()` call, a `__unset()` +/// call or a dynamic-property removal, so the message lists the shapes that do lower +/// directly and then names the one shape users hit most. +/// +/// THE UNTYPED FIXED SLOT is that shape. `unset($obj->untypedProp)` on a property +/// declared without a type (`public $foo = 1;`) truly REMOVES it in PHP: a later read +/// warns `Undefined property` and answers `null`, and a later write recreates it. +/// elephc gives each declared property a fixed, monomorphically typed slot, so a +/// property the checker typed `Int` has no encoding for "removed and reading as null" +/// — every candidate encoding answers `int(0)` or a raw marker word instead. A loud +/// error beats a wrong value, so the shape is refused here. Untyped properties whose +/// storage is a DYNAMIC hash (`stdClass`, undeclared names on +/// `#[AllowDynamicProperties]` classes) are genuinely removable and lower fine. pub(super) fn lower_unset_builtin( _ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { Err(CodegenIrError::unsupported(format!( - "unset target shape with {} lowered operands", + "unset target shape with {} lowered operands (supported: variables, \ + array/hash elements, ArrayAccess offsets, __unset()-backed properties, \ + declared typed object properties, and dynamic object properties). \ + An UNTYPED declared property (`public $p = 1;`) is not supported: its fixed \ + slot has no representation for PHP's removed-then-null read", inst.operands.len() ))) } diff --git a/src/codegen/lower_inst/callables.rs b/src/codegen/lower_inst/callables.rs index 245f00f701..a77dd602f4 100644 --- a/src/codegen/lower_inst/callables.rs +++ b/src/codegen/lower_inst/callables.rs @@ -18,7 +18,7 @@ use crate::codegen::{ emit_release_pushed_refcounted_temp_after_array_push, }; use crate::ir::{Instruction, Op, ValueDef, ValueId}; -use crate::names::{function_symbol, method_symbol, php_symbol_key}; +use crate::names::{function_symbol, label_fragment, method_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::{FunctionSig, PhpType}; @@ -2918,13 +2918,6 @@ fn runtime_string_result_type_supported(result_ty: &PhpType, return_ty: &PhpType result_ty == return_ty || matches!(result_ty, PhpType::Mixed | PhpType::Union(_)) } -/// Converts arbitrary PHP function names into assembly-label-safe fragments. -fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} /// Emits one branch comparing the saved callable name with a candidate function name. fn emit_branch_if_runtime_callable_name_matches( diff --git a/src/codegen/lower_inst/comparisons.rs b/src/codegen/lower_inst/comparisons.rs index 6eb0c969d1..5475fe41e3 100644 --- a/src/codegen/lower_inst/comparisons.rs +++ b/src/codegen/lower_inst/comparisons.rs @@ -304,6 +304,8 @@ pub(super) fn lower_loose_eq( } else if loose_intish_comparable(&lhs_ty, &rhs_ty) { let compare_truthiness = lhs_ty == PhpType::Bool || rhs_ty == PhpType::Bool; emit_intish_compare(ctx, lhs, rhs, is_equal, compare_truthiness)?; + } else if runtime_loose_comparable(&lhs_ty) && runtime_loose_comparable(&rhs_ty) { + emit_mixed_loose_compare(ctx, lhs, &lhs_ty, rhs, &rhs_ty, is_equal)?; } else { return Err(CodegenIrError::unsupported(format!( "{} for PHP types {:?} and {:?}", @@ -315,6 +317,70 @@ pub(super) fn lower_loose_eq( store_if_result(ctx, inst) } +/// Returns true when a PHP type can be boxed into a `Mixed` cell and handed to +/// `__rt_mixed_loose_eq`, the runtime implementation of PHP's full `==` table. +/// +/// Compiler-only storage (raw pointers, buffers, packed structs) has no PHP value +/// identity, so those keep the "unsupported" diagnostic instead of silently +/// comparing as integers. +fn runtime_loose_comparable(ty: &PhpType) -> bool { + !matches!( + ty, + PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) + ) +} + +/// Emits PHP loose equality through `__rt_mixed_loose_eq` for operand pairs whose +/// rule cannot be decided from the static types alone: object vs object, array vs +/// array, array vs anything, and every combination involving a boxed `Mixed`. +/// +/// Concrete operands are boxed into temporary `Mixed` cells first (the same +/// protocol `emit_mixed_strict_compare` uses) and released afterwards; operands +/// that already are `Mixed` are passed straight through and stay borrowed. +fn emit_mixed_loose_compare( + ctx: &mut FunctionContext<'_>, + lhs: ValueId, + lhs_ty: &PhpType, + rhs: ValueId, + rhs_ty: &PhpType, + is_equal: bool, +) -> Result<()> { + let left_box_temp = !is_mixed_like(lhs_ty); + let right_box_temp = !is_mixed_like(rhs_ty); + materialize_value_as_mixed(ctx, lhs, lhs_ty)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + materialize_value_as_mixed(ctx, rhs, rhs_ty)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "x0", 16); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_call_label(ctx.emitter, "__rt_mixed_loose_eq"); + if !is_equal { + ctx.emitter.instruction("eor x0, x0, #1"); // invert the mixed loose-equality result for != + } + } + Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 16); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 0); + abi::emit_call_label(ctx.emitter, "__rt_mixed_loose_eq"); + if !is_equal { + ctx.emitter.instruction("xor rax, 1"); // invert the mixed loose-equality result for != + } + } + } + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + if left_box_temp { + decref_mixed_temp_at(ctx, 32); + } + if right_box_temp { + decref_mixed_temp_at(ctx, 16); + } + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + abi::emit_release_temporary_stack(ctx.emitter, 32); + Ok(()) +} + /// Returns true when loose equality must compare a bool with a string by PHP truthiness. fn string_bool_comparable(lhs_ty: &PhpType, rhs_ty: &PhpType) -> bool { matches!((lhs_ty, rhs_ty), (PhpType::Bool, PhpType::Str) | (PhpType::Str, PhpType::Bool)) diff --git a/src/codegen/lower_inst/conversions.rs b/src/codegen/lower_inst/conversions.rs index af530d2a69..5a96dc4b42 100644 --- a/src/codegen/lower_inst/conversions.rs +++ b/src/codegen/lower_inst/conversions.rs @@ -12,7 +12,7 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; use crate::ir::{Immediate, Instruction, IrType, ValueId}; -use crate::names::method_symbol; +use crate::names::{label_fragment, method_symbol}; use crate::types::PhpType; use super::super::context::FunctionContext; @@ -236,7 +236,7 @@ fn emit_mixed_string_context( .map(|candidate| { ctx.next_label(&format!( "mixed_string_{}", - super::label_fragment(&candidate.class_name) + label_fragment(&candidate.class_name) )) }) .collect::>(); diff --git a/src/codegen/lower_inst/enums.rs b/src/codegen/lower_inst/enums.rs index 2736d7e4f7..9fb94001e5 100644 --- a/src/codegen/lower_inst/enums.rs +++ b/src/codegen/lower_inst/enums.rs @@ -652,11 +652,11 @@ fn emit_float_payload_to_int(ctx: &mut FunctionContext<'_>, bits_reg: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.instruction(&format!("fmov d0, {}", bits_reg)); // move the raw double bits into the float register - ctx.emitter.instruction("fcvtzs x0, d0"); // truncate the double toward zero into the int result + abi::emit_php_float_to_int(ctx.emitter, "x0"); } Arch::X86_64 => { ctx.emitter.instruction(&format!("movq xmm0, {}", bits_reg)); // move the raw double bits into the float register - ctx.emitter.instruction("cvttsd2si rax, xmm0"); // truncate the double toward zero into the int result + abi::emit_php_float_to_int(ctx.emitter, "rax"); } } } diff --git a/src/codegen/lower_inst/exceptions.rs b/src/codegen/lower_inst/exceptions.rs index 1a42b50489..be9b0e80e9 100644 --- a/src/codegen/lower_inst/exceptions.rs +++ b/src/codegen/lower_inst/exceptions.rs @@ -16,6 +16,14 @@ //! synthesized by a codegen guard rather than by a user `new`, and the message string is baked //! at emit time from a caller that passes no span — so there is no origin to print. Reference //! PHP does report one here (the operation's own line), which stays a known gap. +//! - `emit_value_error_unless()` is the shared builtin argument-range guard: it keeps +//! out-of-range arguments (empty separators, non-positive lengths, negative counts, +//! oversized array lengths) from ever reaching a runtime helper that would read +//! uninitialized memory, allocate an unrepresentable size, or loop forever, and raises +//! reference PHP's catchable `ValueError` instead. +//! - `emit_value_error_from_string_result()` is the same guard outcome for php-src's +//! `ValueError`s that interpolate the offending values into their wording, where the caller +//! has already built the exact message at runtime. use crate::codegen::abi; use crate::codegen::platform::Arch; @@ -35,6 +43,188 @@ pub(super) fn emit_type_error(ctx: &mut FunctionContext<'_>, message: &str) { emit_static_exception(ctx, "TypeError", "_spl_type_error_class_id", message); } +/// Throws a catchable PHP `ValueError` carrying a static message. +/// +/// Reference PHP raises this `Error` subclass — not a fatal — when a builtin argument has +/// the right type but a value the function cannot honor (`str_pad()` with an empty pad +/// string, `str_split()` with a non-positive chunk length, `str_repeat()` with a negative +/// count, `explode()` with an empty separator, `array_fill()` with a negative count, +/// `random_int()` with `$min > $max`). `catch (ValueError $e)`, `catch (Error $e)`, and +/// `catch (Throwable $e)` all match; callers pass php-src's own verbatim wording. +pub(super) fn emit_value_error(ctx: &mut FunctionContext<'_>, message: &str) { + emit_static_exception(ctx, "ValueError", "_spl_value_error_class_id", message); +} + +/// The register condition a materialized builtin argument must satisfy to skip its +/// `ValueError`. +/// +/// The register is inspected right before the runtime helper call, while the argument +/// still sits in its target ABI register, so the same guard works for every supported +/// target without re-materializing the operand. +pub(super) enum ValueGuard<'a> { + /// The 64-bit register, read as a signed integer, must be `>= minimum` + /// (`str_split()` chunk length, `str_repeat()`/`array_fill()` counts). + SignedAtLeast(&'a str, i64), + /// The 64-bit register, read as a signed integer, must be `<= maximum` + /// (`array_fill()`'s `$count` ceiling). + /// + /// The bound is materialized into a scratch register first, so a limit wider than the + /// target's compare-immediate encoding (`INT_MAX` does not fit AArch64's 12-bit form) + /// is still checked exactly instead of being truncated by the assembler. + SignedAtMost(&'a str, i64), + /// The 64-bit register, read as a signed integer, must satisfy + /// `-maximum <= value <= maximum` (`array_pad()`'s `$length` magnitude). + /// + /// The bound is checked on the signed argument itself rather than on `abs(value)` + /// so `PHP_INT_MIN`, whose magnitude is not representable, fails the guard instead + /// of wrapping back to a negative "absolute" length. + SignedMagnitudeAtMost(&'a str, i64), + /// The 64-bit register, read as a signed integer, must satisfy + /// `minimum <= value <= maximum` (`round()`'s `$mode` enumeration). + /// + /// Both ends are inclusive; the guard is used for builtin arguments whose accepted + /// values are a small contiguous set of PHP constants rather than a magnitude limit. + SignedInRange(&'a str, i64, i64), + /// The 64-bit register must not hold the given immediate (`range()`'s zero `$step`). + NotEqualToImmediate(&'a str, i64), + /// The first register, read as a signed integer, must be `>= 0` unless the second + /// register is signed-greater-or-equal to the third (`range()`'s `$step` sign rule). + /// + /// PHP only rejects a negative `$step` for an INCREASING range: `range(5, 1, -2)` is + /// valid while `range(1, 5, -2)` is a `ValueError`. The guard therefore passes as soon + /// as `start >= end`, and only then checks the sign of the step. + NonNegativeUnlessSignedBelow(&'a str, &'a str, &'a str), + /// `|first|` must not exceed the UNSIGNED width the second and third registers span, + /// unless those two are equal (`range()`'s `$step` magnitude rule). + /// + /// PHP rejects a `$step` wider than the interval its endpoints span, but a degenerate + /// `range($x, $x, $step)` always yields `[$x]` no matter how large the step is, so an + /// equal pair short-circuits the check. The unsigned comparison makes `PHP_INT_MIN` + /// (whose negation is itself) read as wider than every span instead of wrapping back + /// into a negative "magnitude" that would slip past a signed compare. + /// + /// The endpoints are ordered before the width is taken, and the subtraction that follows + /// is read as unsigned, exactly like php-src's `(zend_ulong) (high - low)`. Taking a + /// signed absolute of the raw difference instead would report `range(PHP_INT_MIN, + /// PHP_INT_MAX, 2)` as a span of `1` and reject it, where PHP spans `2^64 - 1` and goes on + /// to reject the range for its size instead. + MagnitudeWithinSpan(&'a str, &'a str, &'a str), +} + +/// Throws a catchable PHP `ValueError` unless the guarded register satisfies `guard`. +/// +/// Emits the compare/branch pair for the active target, falls through to the throw +/// sequence when the guard fails, and leaves the caller's continuation label in place so +/// the runtime helper call that follows only ever runs with an in-range argument. +pub(super) fn emit_value_error_unless( + ctx: &mut FunctionContext<'_>, + guard: ValueGuard<'_>, + message: &str, +) { + let ok_label = ctx.next_label("value_guard_ok"); + match (ctx.emitter.target.arch, &guard) { + (Arch::AArch64, ValueGuard::SignedAtLeast(reg, minimum)) => { + ctx.emitter.instruction(&format!("cmp {}, #{}", reg, minimum)); // compare the materialized argument against its PHP minimum + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // an argument at or above the minimum is in range + } + (Arch::X86_64, ValueGuard::SignedAtLeast(reg, minimum)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", reg, minimum)); // compare the materialized argument against its PHP minimum + ctx.emitter.instruction(&format!("jge {}", ok_label)); // an argument at or above the minimum is in range + } + (Arch::AArch64, ValueGuard::SignedAtMost(reg, maximum)) => { + abi::emit_load_int_immediate(ctx.emitter, "x9", *maximum); + ctx.emitter.instruction(&format!("cmp {}, x9", reg)); // compare the materialized argument against its PHP maximum + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // an argument at or below the maximum is in range + } + (Arch::X86_64, ValueGuard::SignedAtMost(reg, maximum)) => { + abi::emit_load_int_immediate(ctx.emitter, "r10", *maximum); + ctx.emitter.instruction(&format!("cmp {}, r10", reg)); // compare the materialized argument against its PHP maximum + ctx.emitter.instruction(&format!("jle {}", ok_label)); // an argument at or below the maximum is in range + } + (Arch::AArch64, ValueGuard::SignedMagnitudeAtMost(reg, maximum)) => { + let fail_label = ctx.next_label("value_guard_fail"); + ctx.emitter.instruction(&format!("mov x9, #{}", maximum)); // materialize the largest magnitude PHP accepts for this argument + ctx.emitter.instruction(&format!("cmp {}, x9", reg)); // compare the materialized argument against the positive bound + ctx.emitter.instruction(&format!("b.gt {}", fail_label)); // a value above the bound is out of range + ctx.emitter.instruction(&format!("cmn {}, x9", reg)); // compare the materialized argument against the negated bound + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // a value at or above the negated bound is in range + ctx.emitter.label(&fail_label); + } + (Arch::X86_64, ValueGuard::SignedMagnitudeAtMost(reg, maximum)) => { + let fail_label = ctx.next_label("value_guard_fail"); + ctx.emitter.instruction(&format!("cmp {}, {}", reg, maximum)); // compare the materialized argument against the positive bound + ctx.emitter.instruction(&format!("jg {}", fail_label)); // a value above the bound is out of range + ctx.emitter.instruction(&format!("cmp {}, -{}", reg, maximum)); // compare the materialized argument against the negated bound + ctx.emitter.instruction(&format!("jge {}", ok_label)); // a value at or above the negated bound is in range + ctx.emitter.label(&fail_label); + } + (Arch::AArch64, ValueGuard::SignedInRange(reg, minimum, maximum)) => { + let fail_label = ctx.next_label("value_guard_fail"); + ctx.emitter.instruction(&format!("cmp {}, #{}", reg, minimum)); // compare the materialized argument against the inclusive lower bound + ctx.emitter.instruction(&format!("b.lt {}", fail_label)); // a value below the range is rejected + ctx.emitter.instruction(&format!("cmp {}, #{}", reg, maximum)); // compare the materialized argument against the inclusive upper bound + ctx.emitter.instruction(&format!("b.le {}", ok_label)); // a value at or below the upper bound is in range + ctx.emitter.label(&fail_label); + } + (Arch::X86_64, ValueGuard::SignedInRange(reg, minimum, maximum)) => { + let fail_label = ctx.next_label("value_guard_fail"); + ctx.emitter.instruction(&format!("cmp {}, {}", reg, minimum)); // compare the materialized argument against the inclusive lower bound + ctx.emitter.instruction(&format!("jl {}", fail_label)); // a value below the range is rejected + ctx.emitter.instruction(&format!("cmp {}, {}", reg, maximum)); // compare the materialized argument against the inclusive upper bound + ctx.emitter.instruction(&format!("jle {}", ok_label)); // a value at or below the upper bound is in range + ctx.emitter.label(&fail_label); + } + (Arch::AArch64, ValueGuard::NotEqualToImmediate(reg, forbidden)) => { + ctx.emitter.instruction(&format!("cmp {}, #{}", reg, forbidden)); // compare the materialized argument against the value PHP forbids + ctx.emitter.instruction(&format!("b.ne {}", ok_label)); // any other value is accepted + } + (Arch::X86_64, ValueGuard::NotEqualToImmediate(reg, forbidden)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", reg, forbidden)); // compare the materialized argument against the value PHP forbids + ctx.emitter.instruction(&format!("jne {}", ok_label)); // any other value is accepted + } + (Arch::AArch64, ValueGuard::NonNegativeUnlessSignedBelow(reg, low, high)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", low, high)); // is the interval decreasing or degenerate? + ctx.emitter.instruction(&format!("b.ge {}", ok_label)); // a decreasing interval accepts either step sign + ctx.emitter.instruction(&format!("cmp {}, #0", reg)); // an increasing interval needs a positive step + ctx.emitter.instruction(&format!("b.gt {}", ok_label)); // a strictly positive step is in range + } + (Arch::X86_64, ValueGuard::NonNegativeUnlessSignedBelow(reg, low, high)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", low, high)); // is the interval decreasing or degenerate? + ctx.emitter.instruction(&format!("jge {}", ok_label)); // a decreasing interval accepts either step sign + ctx.emitter.instruction(&format!("cmp {}, 0", reg)); // an increasing interval needs a positive step + ctx.emitter.instruction(&format!("jg {}", ok_label)); // a strictly positive step is in range + } + (Arch::AArch64, ValueGuard::MagnitudeWithinSpan(reg, low, high)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", low, high)); // is the interval degenerate? + ctx.emitter.instruction(&format!("b.eq {}", ok_label)); // a single-point interval accepts any step magnitude + ctx.emitter.instruction(&format!("csel x9, {}, {}, le", low, high)); // x9 = the smaller of the two endpoints + ctx.emitter.instruction(&format!("csel x10, {}, {}, le", high, low)); // x10 = the larger of the two endpoints + ctx.emitter.instruction("sub x9, x10, x9"); // x9 = high - low, the spanned interval as an unsigned width + ctx.emitter.instruction(&format!("cmp {}, #0", reg)); // is the guarded argument negative? + ctx.emitter.instruction(&format!("cneg x10, {}, lt", reg)); // x10 = |argument|, its unsigned magnitude + ctx.emitter.instruction("cmp x10, x9"); // compare the argument magnitude against the spanned width + ctx.emitter.instruction(&format!("b.ls {}", ok_label)); // an unsigned magnitude within the span is in range + } + (Arch::X86_64, ValueGuard::MagnitudeWithinSpan(reg, low, high)) => { + ctx.emitter.instruction(&format!("cmp {}, {}", low, high)); // is the interval degenerate? + ctx.emitter.instruction(&format!("je {}", ok_label)); // a single-point interval accepts any step magnitude + ctx.emitter.instruction(&format!("mov r10, {}", low)); // stage the first endpoint before ordering the pair + ctx.emitter.instruction(&format!("mov r11, {}", high)); // stage the second endpoint before ordering the pair + ctx.emitter.instruction(&format!("cmovg r10, {}", high)); // r10 = the smaller of the two endpoints + ctx.emitter.instruction(&format!("cmovg r11, {}", low)); // r11 = the larger of the two endpoints + ctx.emitter.instruction("sub r11, r10"); // r11 = high - low, the spanned interval as an unsigned width + ctx.emitter.instruction(&format!("mov r10, {}", reg)); // stage the guarded argument before normalizing its magnitude + ctx.emitter.instruction("neg r10"); // negate the guarded argument so a negative one yields its magnitude + ctx.emitter.instruction(&format!("test {}, {}", reg, reg)); // is the guarded argument negative? + ctx.emitter.instruction(&format!("cmovns r10, {}", reg)); // r10 = |argument|, its unsigned magnitude + ctx.emitter.instruction("cmp r10, r11"); // compare the argument magnitude against the spanned width + ctx.emitter.instruction(&format!("jbe {}", ok_label)); // an unsigned magnitude within the span is in range + } + } + emit_value_error(ctx, message); + ctx.emitter.label(&ok_label); +} + /// Throws a catchable PHP `DivisionByZeroError` carrying a static message. /// /// Reference PHP raises this `ArithmeticError` subclass — not a bare fatal — for a @@ -50,16 +240,47 @@ pub(super) fn emit_division_by_zero_error(ctx: &mut FunctionContext<'_>, message ); } +/// Throws a catchable PHP `ArithmeticError` carrying a static message. +/// +/// Reference PHP raises this for arithmetic that has no representable result but is not a +/// division by zero — currently `<<`/`>>` with a negative shift count +/// (`ArithmeticError: Bit shift by negative number`). `catch (ArithmeticError $e)`, +/// `catch (Error $e)`, and `catch (Throwable $e)` all match; `DivisionByZeroError` does not. +pub(super) fn emit_arithmetic_error(ctx: &mut FunctionContext<'_>, message: &str) { + emit_static_exception( + ctx, + "ArithmeticError", + "_spl_arithmetic_error_class_id", + message, + ); +} + /// Throws a catchable PHP `Error` whose message is a runtime string value. pub(super) fn emit_error_value(ctx: &mut FunctionContext<'_>, message: ValueId) -> Result<()> { let (message_ptr_reg, message_len_reg) = abi::string_result_regs(ctx.emitter); ctx.load_string_value_to_regs(message, message_ptr_reg, message_len_reg)?; abi::emit_push_reg_pair(ctx.emitter, message_ptr_reg, message_len_reg); - emit_uncaught_dynamic_error_fatal_if_no_handler(ctx); - emit_dynamic_error_object(ctx); + emit_uncaught_dynamic_throwable_fatal_if_no_handler(ctx, "Error"); + emit_dynamic_throwable_object(ctx, "_spl_error_class_id"); Ok(()) } +/// Throws a catchable PHP `ValueError` whose message already sits in the string-result registers. +/// +/// The static `emit_value_error()` covers the builtin guards whose wording is fixed. A few of +/// php-src's own `ValueError`s interpolate the offending values instead — `range()`'s +/// `"The supplied range exceeds the maximum array size: start=… end=… step=…"` is the one that +/// reaches here — so the caller builds the exact message at runtime and hands it over as a +/// persisted pointer/length pair. The uncaught diagnostic names `ValueError` just like the static +/// path, so an unhandled oversized range still reports PHP's error class rather than the +/// unwinder's generic fallback. +pub(super) fn emit_value_error_from_string_result(ctx: &mut FunctionContext<'_>) { + let (message_ptr_reg, message_len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_push_reg_pair(ctx.emitter, message_ptr_reg, message_len_reg); + emit_uncaught_dynamic_throwable_fatal_if_no_handler(ctx, "ValueError"); + emit_dynamic_throwable_object(ctx, "_spl_value_error_class_id"); +} + /// Allocates one built-in throwable and transfers control to the standard unwinder. fn emit_static_exception( ctx: &mut FunctionContext<'_>, @@ -144,10 +365,18 @@ fn emit_uncaught_exception_fatal_if_no_handler( ctx.emitter.label(&throw_label); } -/// Writes an uncaught dynamic `Error` diagnostic, or continues when a handler exists. -fn emit_uncaught_dynamic_error_fatal_if_no_handler(ctx: &mut FunctionContext<'_>) { +/// Writes an uncaught dynamic throwable diagnostic, or continues when a handler exists. +/// +/// `class_name` names the PHP class in the fatal line (`Error`, `ValueError`, …); the message +/// itself is read from the 16-byte temporary the caller pushed, so this works for any throwable +/// whose text is only known at runtime. +fn emit_uncaught_dynamic_throwable_fatal_if_no_handler( + ctx: &mut FunctionContext<'_>, + class_name: &str, +) { let throw_label = ctx.next_label("dynamic_error_throw"); - let (prefix_label, prefix_len) = ctx.data.add_string(b"Fatal error: Uncaught Error: "); + let prefix = format!("Fatal error: Uncaught {}: ", class_name); + let (prefix_label, prefix_len) = ctx.data.add_string(prefix.as_bytes()); let (suffix_label, suffix_len) = ctx.data.add_string(b"\n"); match ctx.emitter.target.arch { Arch::AArch64 => { @@ -192,8 +421,12 @@ fn emit_uncaught_dynamic_error_fatal_if_no_handler(ctx: &mut FunctionContext<'_> ctx.emitter.label(&throw_label); } -/// Allocates a built-in `Error` that owns the runtime message stored on the stack. -fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { +/// Allocates a built-in throwable that owns the runtime message stored on the stack. +/// +/// `class_id_symbol` selects the built-in class the object reports (`_spl_error_class_id`, +/// `_spl_value_error_class_id`, …). The message pointer/length come from the 16-byte temporary +/// the caller pushed, which is released once both words have been copied into the object. +fn emit_dynamic_throwable_object(ctx: &mut FunctionContext<'_>, class_id_symbol: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_int_immediate(ctx.emitter, "x0", 56); // compact Throwable: message/code/previous @@ -201,8 +434,8 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction("mov x9, #6"); // heap kind 6 = throwable object instance ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the allocation as a runtime object ctx.emitter.instruction("bl __rt_object_handle_acquire"); // bind the new object to its PHP object handle - abi::emit_load_symbol_to_reg(ctx.emitter, "x9", "_spl_error_class_id", 0); - ctx.emitter.instruction("str x9, [x0]"); // store the built-in Error class id + abi::emit_load_symbol_to_reg(ctx.emitter, "x9", class_id_symbol, 0); + ctx.emitter.instruction("str x9, [x0]"); // store the built-in throwable class id abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 0); ctx.emitter.instruction("str x9, [x0, #8]"); // store the runtime exception message pointer abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 8); @@ -220,8 +453,8 @@ fn emit_dynamic_error_object(ctx: &mut FunctionContext<'_>) { ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation as a runtime object ctx.emitter.instruction("call __rt_object_handle_acquire"); // bind the new object to its PHP object handle - abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_error_class_id", 0); - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the built-in Error class id + abi::emit_load_symbol_to_reg(ctx.emitter, "r10", class_id_symbol, 0); + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the built-in throwable class id abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 0); ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the runtime exception message pointer abi::emit_load_temporary_stack_slot(ctx.emitter, "r10", 8); diff --git a/src/codegen/lower_inst/generator_instructions.rs b/src/codegen/lower_inst/generator_instructions.rs index 75ca47e188..cfd9b21827 100644 --- a/src/codegen/lower_inst/generator_instructions.rs +++ b/src/codegen/lower_inst/generator_instructions.rs @@ -18,6 +18,11 @@ use super::*; /// `__rt_gen_suspend(key, value)`; a NULL key requests an auto-increment /// integer key. The helper's result register holds the value delivered by the /// next `send()`/`next()`, which becomes the SSA result of the yield. +/// +/// An `Immediate::Bool(true)` marks a *delegated* yield emitted by the +/// `yield from ` desugaring. Those keys are forwarded verbatim, so the +/// call targets `__rt_gen_suspend_delegated`, which skips PHP's auto-key +/// bookkeeping exactly like `__rt_gen_delegate` does for inner generators. pub(super) fn lower_generator_yield(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let target = ctx.emitter.target; let key_arg = abi::int_arg_reg_name(target, 0); @@ -54,7 +59,12 @@ pub(super) fn lower_generator_yield(ctx: &mut FunctionContext<'_>, inst: &Instru } abi::emit_pop_reg(ctx.emitter, value_arg); - abi::emit_call_label(ctx.emitter, "__rt_gen_suspend"); + let suspend_symbol = if matches!(inst.immediate, Some(Immediate::Bool(true))) { + "__rt_gen_suspend_delegated" + } else { + "__rt_gen_suspend" + }; + abi::emit_call_label(ctx.emitter, suspend_symbol); store_call_result(ctx, inst, &PhpType::Mixed) } diff --git a/src/codegen/lower_inst/hashes.rs b/src/codegen/lower_inst/hashes.rs index 7398441dda..7f4fc9b044 100644 --- a/src/codegen/lower_inst/hashes.rs +++ b/src/codegen/lower_inst/hashes.rs @@ -6,8 +6,9 @@ //! - `crate::codegen::lower_inst::lower_instruction()`. //! //! Key details: -//! - Hash writes may copy-on-write or grow the table, so the returned pointer is -//! written back to the source SSA slot and local slot. +//! - Hash writes may copy-on-write or grow the table, so the returned pointer is written back +//! to the source SSA slot and to the place the receiver was READ from (`ReceiverPlace`): a +//! plain frame slot for a local, the reference cell for a by-reference parameter. //! - `HashGetForWrite` is a lookup that also WRITES: it separates the container the //! matching entry holds and republishes it into that entry's value slot, whose //! address comes from `__rt_hash_get`'s entry-address output (issue #580). @@ -16,10 +17,11 @@ use crate::codegen::{ abi, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed, }; use crate::codegen::platform::Arch; -use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; +use crate::ir::{Immediate, Instruction, ValueId}; use crate::types::PhpType; use super::super::context::FunctionContext; +use super::receiver_place::ReceiverPlace; use super::{ emit_mixed_string_for_persistent_store, expect_operand, load_value_to_first_int_arg, store_if_result, @@ -308,8 +310,8 @@ pub(super) fn lower_hash_set(ctx: &mut FunctionContext<'_>, inst: &Instruction) require_hash(hash_ty.clone(), inst)?; let storage_value_ty = assoc_value_type(&hash_ty, inst)?; let value_ty = require_supported_hash_value(ctx.value_php_type(value)?, &storage_value_ty, inst)?; - let source_local = source_load_local_slot(ctx, hash)?; - if let Some(slot) = source_local { + let receiver = ReceiverPlace::resolve(ctx, hash)?; + if let Some(slot) = receiver.slot() { ctx.release_mutated_source_local_owner(slot, hash)?; } match ctx.emitter.target.arch { @@ -317,9 +319,7 @@ pub(super) fn lower_hash_set(ctx: &mut FunctionContext<'_>, inst: &Instruction) Arch::X86_64 => lower_hash_set_x86_64(ctx, hash, key, value, &value_ty, &storage_value_ty)?, } ctx.store_result_value(hash)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, hash)?; - } + receiver.store_back_value(ctx, hash)?; ctx.writeback_global_array_source(hash)?; Ok(()) } @@ -336,8 +336,8 @@ pub(super) fn lower_hash_unset(ctx: &mut FunctionContext<'_>, inst: &Instruction let key = expect_operand(inst, 1)?; let hash_ty = ctx.value_php_type(hash)?; require_hash(hash_ty.clone(), inst)?; - let source_local = source_load_local_slot(ctx, hash)?; - if let Some(slot) = source_local { + let receiver = ReceiverPlace::resolve(ctx, hash)?; + if let Some(slot) = receiver.slot() { ctx.release_mutated_source_local_owner(slot, hash)?; } match ctx.emitter.target.arch { @@ -357,9 +357,7 @@ pub(super) fn lower_hash_unset(ctx: &mut FunctionContext<'_>, inst: &Instruction } } ctx.store_result_value(hash)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, hash)?; - } + receiver.store_back_value(ctx, hash)?; Ok(()) } @@ -371,8 +369,8 @@ pub(super) fn lower_hash_append(ctx: &mut FunctionContext<'_>, inst: &Instructio require_hash(hash_ty.clone(), inst)?; let storage_value_ty = assoc_value_type(&hash_ty, inst)?; let value_ty = require_supported_hash_value(ctx.value_php_type(value)?, &storage_value_ty, inst)?; - let source_local = source_load_local_slot(ctx, hash)?; - if let Some(slot) = source_local { + let receiver = ReceiverPlace::resolve(ctx, hash)?; + if let Some(slot) = receiver.slot() { ctx.release_mutated_source_local_owner(slot, hash)?; } match ctx.emitter.target.arch { @@ -380,9 +378,7 @@ pub(super) fn lower_hash_append(ctx: &mut FunctionContext<'_>, inst: &Instructio Arch::X86_64 => lower_hash_append_x86_64(ctx, hash, value, &value_ty, &storage_value_ty)?, } ctx.store_result_value(hash)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, hash)?; - } + receiver.store_back_value(ctx, hash)?; ctx.writeback_global_array_source(hash)?; Ok(()) } @@ -441,8 +437,8 @@ pub(super) fn lower_hash_spread(ctx: &mut FunctionContext<'_>, inst: &Instructio let source = expect_operand(inst, 1)?; require_hash(ctx.value_php_type(dest)?, inst)?; require_hash(ctx.value_php_type(source)?, inst)?; - let source_local = source_load_local_slot(ctx, dest)?; - if let Some(slot) = source_local { + let receiver = ReceiverPlace::resolve(ctx, dest)?; + if let Some(slot) = receiver.slot() { ctx.release_mutated_source_local_owner(slot, dest)?; } match ctx.emitter.target.arch { @@ -457,9 +453,7 @@ pub(super) fn lower_hash_spread(ctx: &mut FunctionContext<'_>, inst: &Instructio } abi::emit_call_label(ctx.emitter, "__rt_hash_spread"); ctx.store_result_value(dest)?; - if let Some(slot) = source_local { - ctx.store_value_to_local(slot, dest)?; - } + receiver.store_back_value(ctx, dest)?; ctx.writeback_global_array_source(dest)?; Ok(()) } @@ -731,7 +725,7 @@ pub(super) fn materialize_hash_key_aarch64(ctx: &mut FunctionContext<'_>, key: V } PhpType::Float => { ctx.load_value_to_reg(key, "d0")?; - ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "x1"); abi::emit_load_int_immediate(ctx.emitter, "x2", -1); Ok(()) } @@ -767,7 +761,7 @@ pub(super) fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: Va } PhpType::Float => { ctx.load_value_to_reg(key, "xmm0")?; - ctx.emitter.instruction("cvttsd2si rsi, xmm0"); // PHP casts float array keys to integer keys + abi::emit_php_float_to_int(ctx.emitter, "rsi"); abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); Ok(()) } @@ -1722,32 +1716,6 @@ fn require_supported_hash_value( ))) } -/// Returns the stack/local slot loaded by a hash operand when it came from `load_local`. -fn source_load_local_slot(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { - let Some(value_ref) = ctx.function.value(value) else { - return Err(CodegenIrError::missing_entry("value", value.as_raw())); - }; - let ValueDef::Instruction { inst, .. } = value_ref.def else { - return Ok(None); - }; - let Some(inst_ref) = ctx.function.instruction(inst) else { - return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); - }; - // `Op::LoadRefCell` must be recognized alongside `Op::LoadLocal`, exactly as the - // indexed-array twin of this helper does (`lower_inst::arrays::source_load_local_slot`). - // Without it, a hash write through a reference-bound local (`$r = &$a; $r["k"] = 1;`) - // finds no destination slot, so the table pointer `__rt_hash_set` returns is thrown - // away. That pointer changes whenever the table rehashes past its load factor or is - // COW-split, and the stale one keeps being read: a 41-key hash built through a ref - // reported a garbage count instead of 41. - if matches!(inst_ref.op, Op::LoadLocal | Op::LoadRefCell) { - if let Some(Immediate::LocalSlot(slot)) = inst_ref.immediate { - return Ok(Some(slot)); - } - } - Ok(None) -} - /// Returns the capacity immediate attached to a hash allocation. fn expect_capacity(inst: &Instruction) -> Result { match inst.immediate { diff --git a/src/codegen/lower_inst/iterators.rs b/src/codegen/lower_inst/iterators.rs index fb707fcd60..d59a42ebc5 100644 --- a/src/codegen/lower_inst/iterators.rs +++ b/src/codegen/lower_inst/iterators.rs @@ -118,20 +118,12 @@ pub(super) fn lower_iter_start(ctx: &mut FunctionContext<'_>, inst: &Instruction initialize_dynamic_iterable_iterator(ctx, offset, by_ref, source)?; return Ok(()); } - if let IteratorSourceKind::Object { - class_name, - aggregate_class_name: None, - } = &source_kind - { - abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Object(class_name.clone())); - } - if let IteratorSourceKind::Interface { - interface_name, - aggregate_class_name: None, - } = &source_kind - { - abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Object(interface_name.clone())); - } + // -- the loop's reference on an object source is taken by EIR lowering, not here -- + // `IterStart` used to `incref` an `Object`/`Interface` source so the object stayed + // alive for the whole loop, but nothing ever emitted the matching `decref`: every + // `foreach` over an Iterator (a `Generator` included) leaked the object and every + // heap block it owned. `lower_foreach` now wraps a borrowed object source in an + // `Op::Acquire`, which the loop's existing exit/`LoopCleanup` release paths balance. let initial_cursor = match &source_kind { IteratorSourceKind::Indexed { .. } => -1, IteratorSourceKind::Hash => 0, diff --git a/src/codegen/lower_inst/local_stores.rs b/src/codegen/lower_inst/local_stores.rs index 41d00d4b6b..0bf4385ba3 100644 --- a/src/codegen/lower_inst/local_stores.rs +++ b/src/codegen/lower_inst/local_stores.rs @@ -59,8 +59,23 @@ pub(super) fn instruction_for_value<'a>( pub(super) fn lower_store_ref_cell(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let slot = expect_local_slot(inst)?; let value = expect_operand(inst, 0)?; + store_value_through_ref_cell_slot(ctx, slot, value, &inst.result_php_type) +} + +/// Writes `value` into a ref-cell slot, picking the slot's active storage representation. +/// +/// Shared with the mutating array builtins: a by-reference parameter is read with +/// `load_ref_cell`, so a builtin that RELOCATES its receiver (any growth path that reaches +/// `__rt_array_grow`) has to publish the new pointer through the same slot the value came from. +/// Writing only the raw frame slot would leave the caller's variable pointing at freed storage. +pub(super) fn store_value_through_ref_cell_slot( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + value: ValueId, + value_php_type: &PhpType, +) -> Result<()> { if ctx.local_ref_cell_representation_is_definite(slot) { - return store_value_to_ref_cell_as(ctx, slot, value, &inst.result_php_type); + return store_value_to_ref_cell_as(ctx, slot, value, value_php_type); } if !ctx.local_ref_cell_representation_is_dynamic(slot) { return ctx.store_value_to_raw_local(slot, value); @@ -94,7 +109,7 @@ pub(super) fn lower_store_ref_cell(ctx: &mut FunctionContext<'_>, inst: &Instruc } } ctx.emitter.label(&ref_cell); - store_value_to_ref_cell_as(ctx, slot, value, &inst.result_php_type)?; + store_value_to_ref_cell_as(ctx, slot, value, value_php_type)?; ctx.emitter.label(&done); Ok(()) } diff --git a/src/codegen/lower_inst/method_dispatch.rs b/src/codegen/lower_inst/method_dispatch.rs index 5b00006f65..8f2d137866 100644 --- a/src/codegen/lower_inst/method_dispatch.rs +++ b/src/codegen/lower_inst/method_dispatch.rs @@ -313,11 +313,10 @@ pub(super) fn emit_mixed_method_class_dispatch( abi::emit_jump(ctx.emitter, no_match_label); } -/// Returns a label-safe fragment for class names and method metadata keys. -pub(super) fn label_fragment(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} +/// Re-exports the shared label fragmenter so instruction lowering keeps one implementation. +/// +/// `crate::names::label_fragment` is documented as deliberately NON-injective — every +/// non-alphanumeric byte collapses to `_`, so `a_b` and `aéb` collide. A second copy here +/// invited use where uniqueness matters; there is now one definition carrying that warning. +pub(super) use crate::names::label_fragment; diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 40ceaeab43..270e8eb676 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -26,7 +26,8 @@ use crate::codegen::{ }; use crate::intrinsics::IntrinsicCall; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; -use crate::names::{method_symbol, php_symbol_key}; +use crate::codegen_support::dynamic_new::known_dynamic_new_builtin_class_names; +use crate::names::{label_fragment, method_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, InterfaceInfo, PhpType}; @@ -48,8 +49,8 @@ use crate::codegen::literal_defaults::{ emit_boxed_bool_literal_to_result, emit_boxed_float_literal_to_result, emit_boxed_int_literal_to_result, emit_boxed_null_literal_to_result, emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, - emit_string_literal_default_to_result, emit_tagged_null_literal_to_result, - literal_default_value, LiteralDefaultValue, + emit_string_literal_default_to_result, emit_tagged_int_literal_to_result, + emit_tagged_null_literal_to_result, literal_default_value, LiteralDefaultValue, }; use crate::codegen::{CodegenIrError, Result}; @@ -197,5 +198,116 @@ pub(super) use property_resolution::{ emit_boxed_null, emit_nullable_receiver_object_payload, nullable_object_receiver_class, raw_value_php_type, }; -pub(super) use runtime_property_writes::{lower_dynamic_prop_set, lower_prop_set}; +pub(super) use runtime_property_writes::{ + lower_dynamic_prop_set, lower_prop_set, lower_prop_unset, +}; pub(super) use clone_and_spl::lower_object_clone_shallow; + +/// Stamps a declared property slot with the uninitialized-typed-property marker. +/// +/// The payload word is zeroed and the high word receives +/// `UNINITIALIZED_TYPED_PROPERTY_SENTINEL`, the same encoding a typed property without +/// a default carries, so every existing consumer (`PropInitialized`, the read guard, +/// `__rt_obj_prop_name`/`__rt_obj_prop_value`) already understands the state. +fn emit_property_uninitialized_marker( + ctx: &mut FunctionContext<'_>, + slot: &PropertySlot, + base_reg: &str, +) { + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset); + abi::emit_load_int_immediate( + ctx.emitter, + marker_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); + abi::emit_store_to_address(ctx.emitter, marker_reg, base_reg, slot.offset + 8); +} + +/// Removes a dynamic property from the receiver's property hash (`unset($obj->name)`). +/// +/// The receiver stores its dynamic properties in a hash whose pointer lives at +/// `hash_offset` — offset 8 for `stdClass`, just past the fixed slots for an +/// `#[AllowDynamicProperties]` class. `__rt_hash_unset` copy-on-write splits the table, +/// releases the removed key and the boxed `Mixed` value the entry owned, tombstones the +/// slot so other probe chains survive, and returns the unique table pointer, which is +/// stored back into the receiver. Removing an absent key is a no-op inside the helper, +/// so `unset($obj->never_set)` and a repeated `unset()` both behave like PHP. +/// +/// The receiver register is caller-saved, so it is parked on the temporary stack across +/// the helper call and reloaded before the table pointer is stored back. +fn lower_dynamic_prop_unset( + ctx: &mut FunctionContext<'_>, + object: ValueId, + property: &str, + hash_offset: usize, +) -> Result<()> { + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let (key_label, key_len) = ctx.data.add_string(property.as_bytes()); + ctx.load_value_to_reg(object, object_reg)?; + abi::emit_push_reg(ctx.emitter, object_reg); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_unset"); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, "x0", object_reg, hash_offset); + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!( + "mov rdi, QWORD PTR [{} + {}]", + object_reg, hash_offset + )); // load the dynamic-property hash pointer from the receiver + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_unset"); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, "rax", object_reg, hash_offset); + } + } + Ok(()) +} + +/// Names the reason a resolved fixed property slot cannot represent PHP's "removed" +/// state, or `None` when the uninitialized marker is a faithful encoding for it. +/// +/// Used only for the diagnostic text; the ordering matters because a slot can be both +/// undeclared and by-reference, and the by-reference storage is the more specific +/// obstacle to report. +fn unset_unsupported_slot_reason(slot: &PropertySlot) -> Option<&'static str> { + if slot.is_packed { + return Some("packed class field"); + } + if slot.is_reference { + return Some("by-reference property"); + } + if !slot.is_declared { + return Some("untyped property slot"); + } + None +} + +/// Writes the tagged scalar currently held in the result registers into a property slot: the +/// payload word at `offset` and the runtime tag word at `offset + 8`, matching the layout the +/// tagged-scalar property load and store helpers use. +fn emit_tagged_scalar_property_default_store( + ctx: &mut FunctionContext<'_>, + object_reg: &str, + offset: usize, +) { + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + object_reg, + offset, + ); + abi::emit_store_to_address( + ctx.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), + object_reg, + offset + 8, + ); +} diff --git a/src/codegen/lower_inst/objects/known_property_reads.rs b/src/codegen/lower_inst/objects/known_property_reads.rs index 3b605fea95..597716a686 100644 --- a/src/codegen/lower_inst/objects/known_property_reads.rs +++ b/src/codegen/lower_inst/objects/known_property_reads.rs @@ -320,6 +320,13 @@ pub(super) fn emit_stdclass_get_call( } /// Lowers a static-name read from an undeclared property on an allow-dynamic class. +/// +/// OWNERSHIP: the miss path boxes a FRESH null cell, so the caller owns and releases the +/// result. `__rt_hash_get` only BORROWS the stored cell, so the hit path has to retain it +/// to match — exactly what `__rt_stdclass_get` does for the same storage. Without the +/// retain each read handed the caller a reference it did not own, and the caller's release +/// eventually freed a live hash entry, after which further reads of that property answered +/// `NULL` (a use-after-free of the removed cell). pub(super) fn lower_allow_dynamic_prop_get( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -341,6 +348,7 @@ pub(super) fn lower_allow_dynamic_prop_get( abi::emit_call_label(ctx.emitter, "__rt_hash_get"); ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // missing dynamic properties read as PHP null ctx.emitter.instruction("mov x0, x1"); // return the boxed Mixed cell stored in the hash entry + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit } Arch::X86_64 => { @@ -354,6 +362,7 @@ pub(super) fn lower_allow_dynamic_prop_get( ctx.emitter.instruction("test rax, rax"); // check whether the dynamic-property key was present ctx.emitter.instruction(&format!("je {}", miss_label)); // missing dynamic properties read as PHP null ctx.emitter.instruction("mov rax, rdi"); // return the boxed Mixed cell stored in the hash entry + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the null fallback after a successful dynamic-property hit } } diff --git a/src/codegen/lower_inst/objects/property_defaults.rs b/src/codegen/lower_inst/objects/property_defaults.rs index ad7559e964..badadc1284 100644 --- a/src/codegen/lower_inst/objects/property_defaults.rs +++ b/src/codegen/lower_inst/objects/property_defaults.rs @@ -125,18 +125,11 @@ pub(super) fn emit_property_default( } LiteralDefaultValue::TaggedNull => { emit_tagged_null_literal_to_result(ctx); - abi::emit_store_to_address( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - object_reg, - default.offset, - ); - abi::emit_store_to_address( - ctx.emitter, - crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), - object_reg, - default.offset + 8, - ); + emit_tagged_scalar_property_default_store(ctx, object_reg, default.offset); + } + LiteralDefaultValue::TaggedInt(value) => { + emit_tagged_int_literal_to_result(ctx, *value); + emit_tagged_scalar_property_default_store(ctx, object_reg, default.offset); } LiteralDefaultValue::BoxedNull => { abi::emit_push_reg(ctx.emitter, object_reg); diff --git a/src/codegen/lower_inst/objects/property_loads.rs b/src/codegen/lower_inst/objects/property_loads.rs index 9b2b2c5acc..d9877aa17b 100644 --- a/src/codegen/lower_inst/objects/property_loads.rs +++ b/src/codegen/lower_inst/objects/property_loads.rs @@ -43,8 +43,16 @@ pub(super) fn emit_property_load( PhpType::TaggedScalar => { let int_reg = abi::int_result_reg(ctx.emitter); let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); - abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); - abi::emit_load_from_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + // Mixed-receiver dispatch hands the object pointer in the integer result register, + // so loading the payload first would overwrite the base before the tag word is read + // and the second load would dereference the payload. Same guard as the `Str` arm. + if base_reg == int_reg { + abi::emit_load_from_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); + } else { + abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); + abi::emit_load_from_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + } } ty if is_pointer_sized_property_type(&ty) => { let int_reg = abi::int_result_reg(ctx.emitter); diff --git a/src/codegen/lower_inst/objects/runtime_property_writes.rs b/src/codegen/lower_inst/objects/runtime_property_writes.rs index 6a098cff96..dea0e2d256 100644 --- a/src/codegen/lower_inst/objects/runtime_property_writes.rs +++ b/src/codegen/lower_inst/objects/runtime_property_writes.rs @@ -418,3 +418,47 @@ pub(super) fn emit_runtime_stdclass_set_for_stacked_name( abi::emit_call_label(ctx.emitter, "__rt_stdclass_set"); Ok(()) } + +/// Lowers `unset($object->property)` for a declared, accessible instance property. +/// +/// PHP removes the property from the instance; a *typed* property becomes +/// "uninitialized" again. elephc renders declared properties from a fixed per-class +/// descriptor and cannot drop a slot, so the slot is stamped with the shared +/// uninitialized-typed-property marker — exactly the state a typed property without +/// a default starts in. `isset()` then answers false, `print_r`/`var_export` skip the +/// property, and a later read raises the "must not be accessed before initialization" +/// diagnostic. Any refcounted payload the slot owned is released first, so the write +/// cannot leak a string/array/object. +/// +/// A property that lives in the receiver's DYNAMIC-property hash instead of a fixed +/// slot — every `stdClass` property, and an undeclared name on an +/// `#[AllowDynamicProperties]` class — is genuinely removable, so it takes the hash +/// removal path and matches PHP exactly: the key disappears, `isset()` answers false, +/// the value renderers stop listing it, and a later write re-appends it. +/// +/// Every other slot shape is REFUSED rather than silently skipped. A by-reference +/// property slot holds an object-owned ref-cell pointer that the destructor still has +/// to free and that a later write would write THROUGH — reviving the alias PHP's +/// `unset()` just broke — so neither zeroing nor keeping the cell reproduces PHP. +/// A packed field and an undeclared slot have no removable storage at all. Skipping +/// them quietly left `isset()` answering `true` after an `unset()`, so they now name +/// themselves instead. +pub(in crate::codegen::lower_inst) fn lower_prop_unset(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let object = expect_operand(inst, 0)?; + let property = property_name_immediate(ctx, inst)?.to_string(); + if let Some(hash_offset) = dynamic_property_hash_offset_for_object(ctx, object, &property)? { + return lower_dynamic_prop_unset(ctx, object, &property, hash_offset); + } + let slot = resolve_property_slot(ctx, object, &property, inst)?; + if let Some(reason) = unset_unsupported_slot_reason(&slot) { + return Err(CodegenIrError::unsupported(format!( + "unset() of {} {}::${}", + reason, slot.class_name, slot.property + ))); + } + let base_reg = abi::symbol_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(object, base_reg)?; + release_previous_property_value(ctx, base_reg, &slot.php_type, slot.offset, None); + emit_property_uninitialized_marker(ctx, &slot, base_reg); + Ok(()) +} diff --git a/src/codegen/lower_inst/receiver_place.rs b/src/codegen/lower_inst/receiver_place.rs new file mode 100644 index 0000000000..bcb12a86ee --- /dev/null +++ b/src/codegen/lower_inst/receiver_place.rs @@ -0,0 +1,124 @@ +//! Purpose: +//! Resolves where a mutating array/hash builtin's by-reference receiver has to be written back, +//! and republishes a possibly-relocated container pointer into that place. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::arrays` (`array_pop`, `array_shift`, `array_unshift`, +//! `array_splice`, the sort/shuffle family, `array_multisort`, the hash link sorters). +//! - `crate::codegen::lower_inst::hashes` (`hash_set`). +//! +//! Key details: +//! - Every mutating container builtin copy-on-write splits its receiver first, and a split +//! RELOCATES the storage. So does any growth path that reaches `__rt_array_grow`. The new +//! pointer has to reach the place the value was READ from, or the caller keeps pointing at +//! storage that was already freed. +//! - A plain local is its own frame slot. A by-reference parameter is read with `load_ref_cell` +//! and must be republished through that slot's ref-cell representation +//! (`store_value_through_ref_cell_slot`), which is exactly what a bare `store_value_to_local` +//! on a raw frame slot would skip. + +use crate::codegen::context::FunctionContext; +use crate::codegen::{CodegenIrError, Result}; +use crate::ir::{Immediate, LocalSlotId, Op, ValueDef, ValueId}; +use crate::types::PhpType; + +/// Where a mutating container builtin's receiver has to be written back. +#[derive(Clone, Copy)] +pub(super) enum ReceiverPlace { + /// The receiver was not loaded from a slot this lowering can write back to. + Opaque, + /// A plain local frame slot. + Local(LocalSlotId), + /// A slot whose value is reached through its ref-cell representation. + RefCell(LocalSlotId), +} + +impl ReceiverPlace { + /// Resolves the slot a receiver value was loaded from, if any. + /// + /// Only the two loads that name a slot qualify: `load_local` for a plain variable and + /// `load_ref_cell` for a by-reference parameter. Anything else (a call result, a property + /// read, a global) has no slot to publish a relocated pointer into. + pub(super) fn resolve(ctx: &FunctionContext<'_>, value: ValueId) -> Result { + let Some(value_ref) = ctx.function.value(value) else { + return Err(CodegenIrError::missing_entry("value", value.as_raw())); + }; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Ok(Self::Opaque); + }; + let Some(inst_ref) = ctx.function.instruction(inst) else { + return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); + }; + let Some(Immediate::LocalSlot(slot)) = inst_ref.immediate else { + return Ok(Self::Opaque); + }; + match inst_ref.op { + Op::LoadLocal => Ok(Self::Local(slot)), + Op::LoadRefCell => Ok(Self::RefCell(slot)), + _ => Ok(Self::Opaque), + } + } + + /// Returns the resolved slot, whichever representation it is reached through. + /// + /// Used by the pre-mutation bookkeeping (`release_mutated_source_local_owner`) that only + /// needs to name the slot; the representation choice belongs to the write-back. + pub(super) fn slot(&self) -> Option { + match self { + Self::Opaque => None, + Self::Local(slot) | Self::RefCell(slot) => Some(*slot), + } + } + + /// Rejects a receiver this lowering could not resolve to a writable slot. + /// + /// Only calls that RELOCATE the receiver need this: a mutation that stays inside the existing + /// payload is correct even for a receiver whose place is opaque. A growth reallocates, and a + /// grown container lives somewhere else, so a receiver with nowhere to publish the new pointer + /// must be refused instead of silently dropping the mutation. + pub(super) fn require_writable(&self, what: &str) -> Result<()> { + match self { + Self::Opaque => Err(CodegenIrError::unsupported(format!( + "{} for a by-reference receiver that is not a local variable slot", + what + ))), + _ => Ok(()), + } + } + + /// Publishes the receiver's current pointer back into the place it was read from. + pub(super) fn store_back( + &self, + ctx: &mut FunctionContext<'_>, + value: ValueId, + value_php_type: &PhpType, + ) -> Result<()> { + match self { + Self::Opaque => Ok(()), + Self::Local(slot) => ctx.store_value_to_local(*slot, value), + Self::RefCell(slot) => super::store_value_through_ref_cell_slot( + ctx, + *slot, + value, + value_php_type, + ), + } + } + + /// Publishes the receiver back using the PHP type EIR recorded for the receiver value. + /// + /// The convenience form for the mutating builtins whose receiver keeps its declared container + /// type across the call, which is all of them: a copy-on-write split or a growth changes the + /// address, never the element representation. + pub(super) fn store_back_value( + &self, + ctx: &mut FunctionContext<'_>, + value: ValueId, + ) -> Result<()> { + if matches!(self, Self::Opaque) { + return Ok(()); + } + let value_ty = ctx.value_php_type(value)?; + self.store_back(ctx, value, &value_ty) + } +} diff --git a/src/codegen/lower_inst/runtime_calls.rs b/src/codegen/lower_inst/runtime_calls.rs index df2e96c22f..976f7c014f 100644 --- a/src/codegen/lower_inst/runtime_calls.rs +++ b/src/codegen/lower_inst/runtime_calls.rs @@ -65,12 +65,13 @@ fn lower_unary_string( fn unary_string_symbol(runtime: UnaryStringRuntime) -> &'static str { match runtime { UnaryStringRuntime::AddSlashes => "__rt_addslashes", - UnaryStringRuntime::Base64Decode => "__rt_base64_decode", UnaryStringRuntime::Base64Encode => "__rt_base64_encode", UnaryStringRuntime::BinToHex => "__rt_bin2hex", UnaryStringRuntime::HexToBin => "__rt_hex2bin", UnaryStringRuntime::HtmlEntityDecode => "__rt_html_entity_decode", UnaryStringRuntime::NlToBr => "__rt_nl2br", + UnaryStringRuntime::QuoteMeta => "__rt_quotemeta", + UnaryStringRuntime::QuotedPrintableEncode => "__rt_quoted_printable_encode", UnaryStringRuntime::RawUrlDecode => "__rt_urldecode", UnaryStringRuntime::RawUrlEncode => "__rt_rawurlencode", UnaryStringRuntime::StripSlashes => "__rt_stripslashes", diff --git a/src/codegen/lower_inst/runtime_functions/group_00.rs b/src/codegen/lower_inst/runtime_functions/group_00.rs index 7de5ed5fb8..10b0cc272e 100644 --- a/src/codegen/lower_inst/runtime_functions/group_00.rs +++ b/src/codegen/lower_inst/runtime_functions/group_00.rs @@ -37,6 +37,9 @@ pub(super) fn lower( RuntimeFnId::ArrayDiff => Some({ crate::codegen::lower_inst::builtins::arrays::lower_array_diff(ctx, inst) }), + RuntimeFnId::ArrayCountValues => Some({ + crate::codegen::lower_inst::builtins::arrays::lower_array_count_values(ctx, inst) + }), RuntimeFnId::ArrayDiffAssoc => Some({ crate::codegen::lower_inst::builtins::arrays::lower_array_diff_assoc(ctx, inst) }), diff --git a/src/codegen/lower_inst/runtime_functions/group_07.rs b/src/codegen/lower_inst/runtime_functions/group_07.rs index 53230fcb81..e5b02b9545 100644 --- a/src/codegen/lower_inst/runtime_functions/group_07.rs +++ b/src/codegen/lower_inst/runtime_functions/group_07.rs @@ -61,6 +61,54 @@ pub(super) fn lower( RuntimeFnId::Cosh => Some({ crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "cosh") }), + RuntimeFnId::Bindec => Some({ + crate::codegen::lower_inst::builtins::strings::lower_base_to_number( + ctx, + inst, + "bindec", + 2, + ) + }), + RuntimeFnId::Hexdec => Some({ + crate::codegen::lower_inst::builtins::strings::lower_base_to_number( + ctx, + inst, + "hexdec", + 16, + ) + }), + RuntimeFnId::Octdec => Some({ + crate::codegen::lower_inst::builtins::strings::lower_base_to_number( + ctx, + inst, + "octdec", + 8, + ) + }), + RuntimeFnId::Decbin => Some({ + crate::codegen::lower_inst::builtins::strings::lower_dec_to_base( + ctx, + inst, + "decbin", + 2, + ) + }), + RuntimeFnId::Dechex => Some({ + crate::codegen::lower_inst::builtins::strings::lower_dec_to_base( + ctx, + inst, + "dechex", + 16, + ) + }), + RuntimeFnId::Decoct => Some({ + crate::codegen::lower_inst::builtins::strings::lower_dec_to_base( + ctx, + inst, + "decoct", + 8, + ) + }), RuntimeFnId::Deg2rad => Some({ crate::codegen::lower_inst::builtins::math::lower_deg2rad(ctx, inst) }), @@ -116,7 +164,7 @@ pub(super) fn lower( crate::codegen::lower_inst::builtins::math::lower_random_int(ctx, inst) }), RuntimeFnId::Round => Some({ - crate::codegen::lower_inst::builtins::math::lower_round(ctx, inst) + crate::codegen::lower_inst::builtins::round_mode::lower_round(ctx, inst) }), RuntimeFnId::Sin => Some({ crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "sin") diff --git a/src/codegen/lower_inst/runtime_functions/group_08.rs b/src/codegen/lower_inst/runtime_functions/group_08.rs index eb8bc53196..57a71d806a 100644 --- a/src/codegen/lower_inst/runtime_functions/group_08.rs +++ b/src/codegen/lower_inst/runtime_functions/group_08.rs @@ -28,6 +28,18 @@ pub(super) fn lower( RuntimeFnId::Tanh => Some({ crate::codegen::lower_inst::builtins::math::lower_unary_libm(ctx, inst, "tanh") }), + RuntimeFnId::ElephcObjectIsEnum => Some({ + crate::codegen::lower_inst::builtins::object_props::lower_object_is_enum(ctx, inst) + }), + RuntimeFnId::ElephcObjectPropCount => Some({ + crate::codegen::lower_inst::builtins::object_props::lower_object_prop_count(ctx, inst) + }), + RuntimeFnId::ElephcObjectPropName => Some({ + crate::codegen::lower_inst::builtins::object_props::lower_object_prop_name(ctx, inst) + }), + RuntimeFnId::ElephcObjectPropValue => Some({ + crate::codegen::lower_inst::builtins::object_props::lower_object_prop_value(ctx, inst) + }), RuntimeFnId::ElephcPtrIsNull => Some({ crate::codegen::lower_inst::builtins::pointers::lower_ptr_is_null(ctx, inst) }), diff --git a/src/codegen/lower_inst/runtime_functions/group_09.rs b/src/codegen/lower_inst/runtime_functions/group_09.rs index a34825d648..0afbe8afb0 100644 --- a/src/codegen/lower_inst/runtime_functions/group_09.rs +++ b/src/codegen/lower_inst/runtime_functions/group_09.rs @@ -35,6 +35,9 @@ pub(super) fn lower( RuntimeFnId::SplObjectId => Some({ crate::codegen::lower_inst::builtins::spl::lower_spl_object_id(ctx, inst) }), + RuntimeFnId::Base64Decode => Some({ + crate::codegen::lower_inst::builtins::strings::lower_base64_decode(ctx, inst) + }), RuntimeFnId::Chop => Some({ crate::codegen::lower_inst::builtins::strings::lower_trim_like( ctx, diff --git a/src/codegen/lower_inst/runtime_functions/group_10.rs b/src/codegen/lower_inst/runtime_functions/group_10.rs index 69ac863586..5ba7352b12 100644 --- a/src/codegen/lower_inst/runtime_functions/group_10.rs +++ b/src/codegen/lower_inst/runtime_functions/group_10.rs @@ -112,12 +112,47 @@ pub(super) fn lower( "__rt_strcmp", ) }), + RuntimeFnId::Strncasecmp => Some({ + crate::codegen::lower_inst::builtins::strings::lower_length_limited_compare( + ctx, + inst, + "strncasecmp", + "__rt_strncasecmp", + ) + }), + RuntimeFnId::Strncmp => Some({ + crate::codegen::lower_inst::builtins::strings::lower_length_limited_compare( + ctx, + inst, + "strncmp", + "__rt_strncmp", + ) + }), + RuntimeFnId::Stripos => Some({ + crate::codegen::lower_inst::builtins::strings::lower_string_position( + ctx, + inst, + "stripos", + "__rt_stripos", + crate::codegen::lower_inst::builtins::strings::StringPositionDirection::Forward, + ) + }), + RuntimeFnId::Strripos => Some({ + crate::codegen::lower_inst::builtins::strings::lower_string_position( + ctx, + inst, + "strripos", + "__rt_strripos", + crate::codegen::lower_inst::builtins::strings::StringPositionDirection::Reverse, + ) + }), RuntimeFnId::Strpos => Some({ crate::codegen::lower_inst::builtins::strings::lower_string_position( ctx, inst, "strpos", "__rt_strpos", + crate::codegen::lower_inst::builtins::strings::StringPositionDirection::Forward, ) }), RuntimeFnId::Strrpos => Some({ @@ -126,6 +161,7 @@ pub(super) fn lower( inst, "strrpos", "__rt_strrpos", + crate::codegen::lower_inst::builtins::strings::StringPositionDirection::Reverse, ) }), RuntimeFnId::Strstr => Some({ @@ -137,6 +173,9 @@ pub(super) fn lower( RuntimeFnId::Substr => Some({ crate::codegen::lower_inst::builtins::strings::lower_substr(ctx, inst) }), + RuntimeFnId::SubstrCount => Some({ + crate::codegen::lower_inst::builtins::strings::lower_substr_count(ctx, inst) + }), RuntimeFnId::SubstrReplace => Some({ crate::codegen::lower_inst::builtins::strings::lower_substr_replace(ctx, inst) }), @@ -153,12 +192,7 @@ pub(super) fn lower( crate::codegen::lower_inst::builtins::strings::lower_ucfirst(ctx, inst) }), RuntimeFnId::Ucwords => Some({ - crate::codegen::lower_inst::builtins::strings::lower_unary_string_runtime( - ctx, - inst, - "ucwords", - "__rt_ucwords", - ) + crate::codegen::lower_inst::builtins::strings::lower_ucwords(ctx, inst) }), RuntimeFnId::Vprintf => Some({ crate::codegen::lower_inst::builtins::strings::lower_vprintf(ctx, inst) diff --git a/src/codegen/lower_inst/runtime_functions/group_12.rs b/src/codegen/lower_inst/runtime_functions/group_12.rs index 56f8bf821a..62a110a1c7 100644 --- a/src/codegen/lower_inst/runtime_functions/group_12.rs +++ b/src/codegen/lower_inst/runtime_functions/group_12.rs @@ -19,6 +19,30 @@ pub(super) fn lower( target: RuntimeFnId, ) -> Option> { match target { + RuntimeFnId::ArrayPtrSeek => Some({ + crate::codegen::lower_inst::builtins::arrays::lower_array_ptr_seek(ctx, inst) + }), + RuntimeFnId::ArrayPtrKey => Some({ + crate::codegen::lower_inst::builtins::arrays::lower_array_ptr_key(ctx, inst) + }), + RuntimeFnId::ArrayPtrValue => Some({ + crate::codegen::lower_inst::builtins::arrays::lower_array_ptr_value(ctx, inst) + }), + RuntimeFnId::BaseConvert => Some({ + crate::codegen::lower_inst::builtins::strings::lower_base_convert(ctx, inst) + }), + RuntimeFnId::ChunkSplit => Some({ + crate::codegen::lower_inst::builtins::strings::lower_chunk_split(ctx, inst) + }), + RuntimeFnId::StrWordCount => Some({ + crate::codegen::lower_inst::builtins::strings::lower_str_word_count(ctx, inst) + }), + RuntimeFnId::CountChars => Some({ + crate::codegen::lower_inst::builtins::strings::lower_count_chars(ctx, inst) + }), + RuntimeFnId::Strtr => Some({ + crate::codegen::lower_inst::builtins::strings::lower_strtr(ctx, inst) + }), RuntimeFnId::MethodExists => Some({ crate::codegen::lower_inst::builtins::lower_member_exists( ctx, @@ -54,6 +78,9 @@ pub(super) fn lower( RuntimeFnId::Gettype => Some({ crate::codegen::lower_inst::builtins::lower_gettype(ctx, inst) }), + RuntimeFnId::IntvalBase => Some({ + crate::codegen::lower_inst::builtins::types::lower_intval_base(ctx, inst) + }), RuntimeFnId::IsCallable => Some({ crate::codegen::lower_inst::builtins::lower_is_callable(ctx, inst) }), diff --git a/src/codegen/lower_inst/static_locals.rs b/src/codegen/lower_inst/static_locals.rs index 54361ccaaf..c1fb23406b 100644 --- a/src/codegen/lower_inst/static_locals.rs +++ b/src/codegen/lower_inst/static_locals.rs @@ -10,6 +10,9 @@ //! so their values persist across function calls without using frame slots. //! - Initializers transfer their freshly-created owner into the static slot; //! assignments retain refcounted values before publishing a second owner. +//! - Both symbols come from `crate::names::static_local_symbol()` / +//! `static_local_init_symbol()`, which encode the (function, variable) pair injectively. +//! Building them by string concatenation merged unrelated statics onto one cell. use crate::codegen::abi; use crate::codegen::platform::Arch; @@ -136,9 +139,8 @@ fn resolve_static_local_slot( CodegenIrError::invalid_module(format!("{} static local is missing a source name", inst.op.name())) })?; let php_type = local.php_type.codegen_repr(); - let function_fragment = static_local_function_fragment(&ctx.function.name); - let symbol = format!("_static_{}_{}", function_fragment, name); - let init_symbol = format!("{}_init", symbol); + let symbol = crate::names::static_local_symbol(&ctx.function.name, &name); + let init_symbol = crate::names::static_local_init_symbol(&ctx.function.name, &name); ctx.data.add_comm(symbol.clone(), 16); ctx.data.add_comm(init_symbol.clone(), 8); // Record this static so the `--web` `__rt_web_reset` routine can release and @@ -233,18 +235,3 @@ fn clear_static_local_high_word_if_needed(ctx: &mut FunctionContext<'_>, slot: & abi::emit_store_zero_to_symbol(ctx.emitter, &slot.symbol, 8); } } - -/// Builds an assembly-safe function fragment for a static-local storage symbol. -fn static_local_function_fragment(name: &str) -> String { - let mut fragment = String::new(); - for ch in name.chars() { - match ch { - 'A'..='Z' | 'a'..='z' | '0'..='9' => fragment.push(ch), - '_' => fragment.push_str("_u_"), - '\\' => fragment.push_str("_N_"), - ':' => fragment.push_str("_C_"), - _ => fragment.push('_'), - } - } - fragment -} diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 14d4e8a977..104973bc75 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -864,6 +864,7 @@ fn value_is_owned_mixed_store_temporary(ctx: &FunctionContext<'_>, value: ValueI crate::ir::Op::ICheckedAdd | crate::ir::Op::ICheckedSub | crate::ir::Op::ICheckedMul + | crate::ir::Op::ICheckedPow | crate::ir::Op::MixedNumericBinop | crate::ir::Op::MixedBox )) diff --git a/src/codegen/lower_inst/strings.rs b/src/codegen/lower_inst/strings.rs index 3d1896ea90..ab19d311f6 100644 --- a/src/codegen/lower_inst/strings.rs +++ b/src/codegen/lower_inst/strings.rs @@ -417,3 +417,33 @@ fn lower_loaded_bool_to_string(ctx: &mut FunctionContext<'_>) -> Result<()> { } Ok(()) } + +/// Lowers `Op::StrIncDec`: PHP's `++` / `--` applied to a string value. +/// +/// The operand is either a concrete `Str` payload or a boxed `Mixed` cell, and the result is +/// ALWAYS a freshly allocated boxed `Mixed` cell because the operator can change the value's +/// type (`"9"++` is `int(10)`, `"az"++` is `"ba"`). A concrete string goes straight to +/// `__rt_str_inc_dec`; a boxed value goes through `__rt_mixed_inc_dec`, which routes a string +/// payload to the same helper and keeps every other payload on the existing numeric path. +/// +/// The `i64` immediate carries the delta and is `+1` for `++` and `-1` for `--`. +pub(super) fn lower_str_inc_dec(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let value = expect_operand(inst, 0)?; + let delta = super::expect_i64(inst)?; + let operand_ty = ctx.value_php_type(value)?.codegen_repr(); + ctx.load_value_to_result(value)?; + let delta_reg = match (ctx.emitter.target.arch, &operand_ty) { + (Arch::AArch64, PhpType::Str) => "x3", + (Arch::AArch64, _) => "x1", + (Arch::X86_64, PhpType::Str) => "rcx", + (Arch::X86_64, _) => "rdi", + }; + abi::emit_load_int_immediate(ctx.emitter, delta_reg, delta); + let helper = if matches!(operand_ty, PhpType::Str) { + "__rt_str_inc_dec" + } else { + "__rt_mixed_inc_dec" + }; + abi::emit_call_label(ctx.emitter, helper); + store_if_result(ctx, inst) +} diff --git a/src/codegen/lower_term.rs b/src/codegen/lower_term.rs index a1cc783f8d..9b3b4791a7 100644 --- a/src/codegen/lower_term.rs +++ b/src/codegen/lower_term.rs @@ -392,22 +392,74 @@ mod tests { assert!(asm.contains("stur x0, [x29, #-8]"), "{asm}"); } - /// Verifies conditional branch arguments use per-edge copy stubs. + /// Verifies conditional branch arguments use per-edge copy stubs: both edges get their own + /// emitted stub label and both branches target the labels that were actually emitted. + /// + /// The assertions are structural on purpose. Label ids come from a module-wide counter, so + /// pinning literal numbers would make this fixture break whenever unrelated label allocation + /// shifts rather than when edge lowering regresses. #[test] fn cond_br_arguments_emit_edge_copy_stubs() { let asm = generate_cond_branch_arg_main_asm(Target::new(Platform::Linux, Arch::X86_64)); - assert!(asm.contains("_eir_main_cond_then_args_0:"), "{asm}"); - assert!(asm.contains("_eir_main_cond_else_args_1:"), "{asm}"); + let then_edge = find_numbered_label(&asm, "_eir_main_cond_then_args"); + let else_edge = find_numbered_label(&asm, "_eir_main_cond_else_args"); + assert_ne!(then_edge, else_edge, "{asm}"); + assert!(branches_to(&asm, &then_edge), "{asm}"); + assert!(branches_to(&asm, &else_edge), "{asm}"); } - /// Verifies switch case and default arguments use per-edge copy stubs. + /// Verifies switch case and default arguments use per-edge copy stubs, with the case compare + /// and the default fallthrough branching to the labels that were actually emitted. + /// + /// Structural for the same reason as `cond_br_arguments_emit_edge_copy_stubs()`. #[test] fn switch_arguments_emit_edge_copy_stubs() { let asm = generate_switch_arg_main_asm(Target::new(Platform::Linux, Arch::AArch64)); - assert!(asm.contains("_eir_main_switch_case_args_0:"), "{asm}"); - assert!(asm.contains("_eir_main_switch_default_args_1:"), "{asm}"); + let case_edge = find_numbered_label(&asm, "_eir_main_switch_case_args"); + let default_edge = find_numbered_label(&asm, "_eir_main_switch_default_args"); + assert_ne!(case_edge, default_edge, "{asm}"); + assert!(branches_to(&asm, &case_edge), "{asm}"); + assert!(branches_to(&asm, &default_edge), "{asm}"); + } + + /// Returns the one emitted assembly label named `_`. + /// + /// Panics unless exactly one such label definition exists, so a fixture that stops emitting an + /// edge stub (or emits it twice) still fails loudly without depending on the counter value. + fn find_numbered_label(asm: &str, prefix: &str) -> String { + let matches: Vec<&str> = asm + .lines() + .filter_map(|line| line.strip_suffix(':')) + .filter(|name| { + name.strip_prefix(prefix) + .and_then(|rest| rest.strip_prefix('_')) + .is_some_and(|digits| { + !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) + }) + }) + .collect(); + assert_eq!( + matches.len(), + 1, + "expected exactly one '{prefix}_:' label, found {matches:?} in:\n{asm}" + ); + matches[0].to_string() + } + + /// Returns true when some instruction other than the label definition branches to `label`. + /// + /// Matches the label as a whole operand word so it stays mnemonic- and target-agnostic + /// (`jne`/`jmp` on x86_64, `b.eq`/`b` on ARM64). + fn branches_to(asm: &str, label: &str) -> bool { + let definition = format!("{label}:"); + asm.lines() + .filter(|line| line.trim() != definition) + .any(|line| { + line.split_whitespace() + .any(|word| word.trim_end_matches(',') == label) + }) } /// Verifies throw terminators publish `_exc_value` and call the exception unwinder. diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 6db3e281c9..18c96745f9 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -32,6 +32,7 @@ mod lower_term; mod runtime_callable_invoker; mod runtime_metadata; mod shared_state; +mod stack_guard; pub mod value_placement; mod web; use runtime_metadata::*; @@ -248,7 +249,7 @@ fn finalize_user_asm( eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); } - let data_output = data.emit(); + let data_output = data.emit(module.target); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); let user_functions = runtime_user_function_sigs(module); @@ -292,6 +293,7 @@ fn finalize_user_asm( // canonicalized string into any program that mentions it, and reference PHP // prints it in every fatal error. module.source_path.as_deref(), + module.target, ); let mut user_asm = emitter.output(); diff --git a/src/codegen/shared_state.rs b/src/codegen/shared_state.rs index a73dfe44b2..331fccf632 100644 --- a/src/codegen/shared_state.rs +++ b/src/codegen/shared_state.rs @@ -9,6 +9,9 @@ //! Key details: //! - Cached labels are global assembly entries emitted at their first call site. //! - Receiver-bearing descriptors cache only immutable templates; each call still captures its object. +//! - Owns the module-wide assembly label counter. It must not be per function: the readable part +//! of a label is a lossy fragment of the PHP function/block name, so only a module-unique +//! trailing id keeps two functions with similar names from emitting the same label. use crate::codegen::callable_dispatch::{RuntimeCallableCase, RuntimeStaticMethodCallableCase}; use crate::types::{FunctionSig, PhpType}; @@ -25,6 +28,7 @@ pub(crate) struct SharedCodegenState { runtime_callable_invokers: Vec, runtime_builtin_wrappers: Vec, runtime_extern_wrappers: Vec, + label_counter: usize, } /// Reusable static descriptor template for one public instance method. @@ -58,6 +62,17 @@ struct RuntimeCallWrapperCacheEntry { } impl SharedCodegenState { + /// Reserves the next module-unique assembly label id. + /// + /// Every generated local label ends in `_` taken from this counter. Because the id is a + /// decimal run terminated by the preceding `_`, it is recoverable from the finished label, + /// which makes the whole label unique no matter how ambiguous its readable prefix is. + pub(super) fn next_label_id(&mut self) -> usize { + let id = self.label_counter; + self.label_counter += 1; + id + } + /// Returns cached runtime string-callable cases for the requested specialization. pub(super) fn runtime_string_descriptor_cases( &self, diff --git a/src/codegen/stack_guard.rs b/src/codegen/stack_guard.rs new file mode 100644 index 0000000000..e388f4f238 --- /dev/null +++ b/src/codegen/stack_guard.rs @@ -0,0 +1,162 @@ +//! Purpose: +//! Emits the per-function half of the call-stack overflow guard: the prologue compare of +//! the stack pointer against the runtime `_stack_limit` floor, and the process-entry call +//! that publishes that floor. +//! +//! Called from: +//! - `crate::codegen::frame::emit_function_prologue_with_label()` for every compiled PHP +//! function, method, closure, and generator body. +//! - `crate::codegen::frame::emit_main_prologue()` and +//! `crate::codegen::frame::emit_web_entry_stub()` for the one-time floor measurement. +//! +//! Key details: +//! - The check runs immediately after the frame has been reserved, so the compare already +//! accounts for this function's own frame; the runtime reserve only has to cover what a +//! single guarded frame can still consume before the next guarded call. +//! - It must be branch-only and must not touch memory below the stack pointer, because it +//! runs when the remaining stack may be a single page. +//! - AArch64 keeps the conditional branch local and reaches `__rt_stack_overflow` with an +//! unconditional `b`: `b.cond` only encodes a ±1 MiB displacement, which large programs +//! exceed, while `b` reaches ±128 MiB and gets linker veneers beyond that. +//! - Registers: only x9 (AArch64 symbol scratch) is clobbered, and nothing at all on +//! x86_64 outside PIC mode. Incoming argument registers are untouched, which is what +//! lets the check sit before the parameter spill loop. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen_support::runtime::STACK_LIMIT_SYMBOL; + +/// Runtime symbol that measures the stack once and publishes `_stack_limit`. +const STACK_LIMIT_INIT_SYMBOL: &str = "__rt_stack_limit_init"; + +/// Runtime symbol that reports the controlled fatal and exits with status 255. +const STACK_OVERFLOW_SYMBOL: &str = "__rt_stack_overflow"; + +/// Emits the one-time call that measures the running stack and publishes the guard floor. +/// +/// Must be emitted after the process-entry prologue has already stored argc/argv, because +/// the helper is an ordinary call and clobbers the C-ABI argument registers. Until it runs, +/// `_stack_limit` is zero and every prologue check passes. +pub(super) fn emit_stack_limit_init_call(emitter: &mut Emitter) { + emitter.comment("publish the call-stack overflow floor for this process"); + abi::emit_call_label(emitter, STACK_LIMIT_INIT_SYMBOL); +} + +/// Emits the prologue stack-depth check for one compiled function. +/// +/// Compares the stack pointer against `_stack_limit` as an unsigned value and branches to +/// `__rt_stack_overflow` when it is below. A zero limit (the pre-initialization state, and +/// the state whenever the floor could not be determined) makes the compare always pass, so +/// the guard is inert rather than wrong when the bounds are unknown. +/// +/// `ok_label` must be a function-unique label; it is emitted immediately after the check on +/// AArch64 and unused on x86_64, whose `jb rel32` reaches the runtime symbol directly. +pub(super) fn emit_stack_limit_check(emitter: &mut Emitter, ok_label: &str) { + emitter.comment("call-stack overflow guard"); + match emitter.target.arch { + Arch::AArch64 => { + if emitter.pic_data_refs { + // PIC builds must reach the limit through the GOT; the shared helper owns + // that sequence and leaves the comparison flags set the same way. + abi::emit_cmp_reg_to_symbol(emitter, "sp", STACK_LIMIT_SYMBOL); + } else { + emitter.adrp("x9", STACK_LIMIT_SYMBOL); + emitter.ldr_lo12("x9", "x9", STACK_LIMIT_SYMBOL); + emitter.instruction("cmp sp, x9"); // is the freshly reserved frame below the published stack floor? + } + emitter.instruction(&format!("b.hs {}", ok_label)); // still above the floor — continue into the function body + emitter.instruction(&format!("b {}", STACK_OVERFLOW_SYMBOL)); // out of stack — report the controlled fatal and exit + emitter.label(ok_label); + } + Arch::X86_64 => { + abi::emit_cmp_reg_to_symbol(emitter, "rsp", STACK_LIMIT_SYMBOL); + emitter.instruction(&format!("jb {}", STACK_OVERFLOW_SYMBOL)); // out of stack — report the controlled fatal and exit + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen::platform::{Platform, Target}; + + /// Emits the prologue check for `target` and returns the generated assembly text. + fn check_asm(target: Target) -> String { + let mut emitter = Emitter::new(target); + emit_stack_limit_check(&mut emitter, "_test_stack_ok"); + emitter.output() + } + + /// macos-aarch64 must compare the stack pointer against `_stack_limit` and reach the + /// fatal through an unconditional branch, so the conditional branch stays local and + /// cannot exceed the AArch64 ±1 MiB `b.cond` displacement in large programs. + #[test] + fn test_prologue_check_macos_aarch64() { + let asm = check_asm(Target::new(Platform::MacOS, Arch::AArch64)); + assert!(asm.contains("adrp x9, _stack_limit@PAGE"), "{asm}"); + assert!(asm.contains("ldr x9, [x9, _stack_limit@PAGEOFF]"), "{asm}"); + assert!(asm.contains("cmp sp, x9"), "{asm}"); + assert!(asm.contains("b.hs _test_stack_ok"), "{asm}"); + assert!(asm.contains("b __rt_stack_overflow"), "{asm}"); + assert!(asm.contains("_test_stack_ok:"), "{asm}"); + } + + /// linux-aarch64 emits the same guard through the ELF `:lo12:` relocation spelling. + #[test] + fn test_prologue_check_linux_aarch64() { + let asm = check_asm(Target::new(Platform::Linux, Arch::AArch64)); + assert!(asm.contains("adrp x9, _stack_limit"), "{asm}"); + assert!(asm.contains("ldr x9, [x9, :lo12:_stack_limit]"), "{asm}"); + assert!(asm.contains("cmp sp, x9"), "{asm}"); + assert!(asm.contains("b.hs _test_stack_ok"), "{asm}"); + assert!(asm.contains("b __rt_stack_overflow"), "{asm}"); + } + + /// linux-x86_64 folds the whole guard into two instructions: a RIP-relative memory + /// compare and a `jb`, whose rel32 displacement always reaches the runtime symbol. + #[test] + fn test_prologue_check_linux_x86_64() { + let asm = check_asm(Target::new(Platform::Linux, Arch::X86_64)); + assert!( + asm.contains("cmp rsp, QWORD PTR [rip + _stack_limit]"), + "{asm}" + ); + assert!(asm.contains("jb __rt_stack_overflow"), "{asm}"); + } + + /// The check must not write memory or touch an argument register: it runs on a frame + /// that may be one page away from the guard page, and before the incoming parameters + /// have been spilled to their slots. + #[test] + fn test_prologue_check_touches_no_memory_or_argument_registers() { + for target in [ + Target::new(Platform::MacOS, Arch::AArch64), + Target::new(Platform::Linux, Arch::AArch64), + Target::new(Platform::Linux, Arch::X86_64), + ] { + let asm = check_asm(target); + for line in asm.lines().map(str::trim) { + assert!( + !line.starts_with("str ") && !line.starts_with("stp ") + && !line.starts_with("stur ") && !line.starts_with("push "), + "guard stored to memory on {target:?}: {line}" + ); + } + for arg_reg in ["x0", "x1", "x2", "rdi", "rsi", "rdx", "rcx", "r8", "r9"] { + assert!( + !asm.contains(&format!(" {arg_reg},")), + "guard clobbered {arg_reg} on {target:?}: {asm}" + ); + } + } + } + + /// The one-time initializer must be a plain call to the runtime measurement helper. + #[test] + fn test_stack_limit_init_call_targets_the_runtime_helper() { + let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_stack_limit_init_call(&mut emitter); + assert!(emitter.output().contains("call __rt_stack_limit_init")); + } +} diff --git a/src/codegen_support/abi/mod.rs b/src/codegen_support/abi/mod.rs index 24547df599..256574de06 100644 --- a/src/codegen_support/abi/mod.rs +++ b/src/codegen_support/abi/mod.rs @@ -59,6 +59,6 @@ pub use symbols::{emit_load_symbol_to_local_slot, emit_store_local_slot_to_symbo pub use values::{ emit_branch_if_int_result_nonzero, emit_branch_if_int_result_zero, emit_decref_if_refcounted, emit_float_result_to_int_result, emit_incref_if_refcounted, emit_int_result_to_float_result, - emit_jump, emit_load, emit_load_int_immediate, emit_release_local_ref_cell, emit_store, - emit_write_stdout, + emit_jump, emit_load, emit_load_int_immediate, emit_php_float_to_int, + emit_release_local_ref_cell, emit_store, emit_write_stdout, }; diff --git a/src/codegen_support/abi/values.rs b/src/codegen_support/abi/values.rs index 6a7937bff8..2d25d8e82e 100644 --- a/src/codegen_support/abi/values.rs +++ b/src/codegen_support/abi/values.rs @@ -278,31 +278,41 @@ pub fn emit_int_result_to_float_result(emitter: &mut Emitter) { } } -/// Truncates the floating-point result register value to the integer result register. +/// Converts the double in the float result register to a PHP `int` in `int_reg`. /// -/// AArch64: `fcvtzs` (floating-point convert to signed fixed-point). x86_64: `cvttsd2si` (convert with truncation). -/// Used when a PHP float must be coerced to int in mixed arithmetic contexts. -pub fn emit_float_result_to_int_result(emitter: &mut Emitter) { - match emitter.target.arch { - crate::codegen_support::platform::Arch::AArch64 => { - let inst = format!( - "fcvtzs {}, {}", - int_result_reg(emitter), - float_result_reg(emitter) - ); - emitter.instruction(&inst); // truncate the floating-point result into the integer result register - } - crate::codegen_support::platform::Arch::X86_64 => { - let inst = format!( - "cvttsd2si {}, {}", - int_result_reg(emitter), - float_result_reg(emitter) - ); - emitter.instruction(&inst); // truncate the floating-point result into the integer result register - } +/// This is the single shared PHP `float`→`int` conversion. Every `(int)` cast, `intval()`, +/// float array key, Mixed unboxing, and numeric-argument coercion must go through it so the +/// supported targets can never diverge again. +/// +/// Raw hardware truncation is *not* PHP-equivalent and is not even consistent across the +/// supported matrix: AArch64 `fcvtzs` saturates (NaN → 0, out of range → `INT64_MIN`/`INT64_MAX`) +/// while x86_64 `cvttsd2si` returns `INT64_MIN` for every invalid input. Reference PHP 8.4 +/// (`zend_dval_to_lval`) maps NaN and ±INF to `0` and reduces any other out-of-range finite +/// double modulo 2^64. `__rt_php_float_to_int` implements exactly that with integer instructions +/// on both targets. +/// +/// The helper returns its value in the symbol scratch register (`x9` / `r11`) and preserves +/// every other register — including the int result register and the floating-point file — so +/// call sites that still hold live values can request any destination register. +pub fn emit_php_float_to_int(emitter: &mut Emitter, int_reg: &str) { + emit_call_label(emitter, "__rt_php_float_to_int"); + let helper_result_reg = match emitter.target.arch { + Arch::AArch64 => "x9", + Arch::X86_64 => "r11", + }; + if int_reg != helper_result_reg { + emitter.instruction(&format!("mov {}, {}", int_reg, helper_result_reg)); // move the PHP integer conversion result into the requested register } } +/// Converts the float result register to a PHP `int` in the integer result register. +/// +/// Thin wrapper over [`emit_php_float_to_int`] for the common "result register to result +/// register" coercion; see that function for the PHP semantics and the register contract. +pub fn emit_float_result_to_int_result(emitter: &mut Emitter) { + emit_php_float_to_int(emitter, int_result_reg(emitter)); +} + /// Loads a 64-bit immediate integer `value` into `reg`. /// /// AArch64: uses `mov` for values in [−65536, 65535]; otherwise constructs the value using diff --git a/src/codegen_support/callable_descriptor.rs b/src/codegen_support/callable_descriptor.rs index 63eea5e4dd..db613fe798 100644 --- a/src/codegen_support/callable_descriptor.rs +++ b/src/codegen_support/callable_descriptor.rs @@ -580,6 +580,7 @@ fn type_tag(ty: &PhpType) -> u64 { #[cfg(test)] mod tests { use super::*; + use crate::codegen_support::platform::{Arch, Platform, Target}; use crate::span::Span; /// Verifies that descriptor records contain signature, environment, and invocation pointers. @@ -624,7 +625,10 @@ mod tests { "call", ), ); - let asm = data.emit(); + let asm = data.emit(Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + }); assert!(asm.contains(&format!(".globl {}\n{}:\n", descriptor, descriptor))); assert!(asm.contains(" .quad _call_entry\n")); @@ -665,7 +669,10 @@ mod tests { CallableDescriptorInvocation::named(CallableDescriptorShape::Function, "demo"), Some("_call_invoker"), ); - let asm = data.emit(); + let asm = data.emit(Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + }); assert!(asm.contains(&format!(".globl {}\n{}:\n", descriptor, descriptor))); assert!(asm.contains(" .quad _call_entry\n")); diff --git a/src/codegen_support/data_section.rs b/src/codegen_support/data_section.rs index 2311aff074..33700e7c7d 100644 --- a/src/codegen_support/data_section.rs +++ b/src/codegen_support/data_section.rs @@ -7,11 +7,51 @@ //! //! Key details: //! - Labels must stay stable within one compilation because code emission references them before final serialization. +//! - `.comm`'s alignment operand is target-dependent and must follow the object format, not the +//! host: Mach-O reads it as a power-of-two exponent, ELF as a byte count. Emitting one spelling +//! everywhere silently under-aligns every common symbol on ELF, which the assembler accepts and +//! the linker then rejects with `relocation truncated to fit` for any 64-bit access. use std::collections::HashMap; +use crate::codegen_support::platform::{Platform, Target}; use crate::types::PhpType; +/// Alignment every common symbol is emitted with: 8 bytes, i.e. `2^3`. +/// +/// Common storage holds pointers, `Mixed` boxes and 64-bit scalars, all of which are reached +/// through 64-bit loads and stores. On AArch64 those assemble to `R_AARCH64_LDST64_ABS_LO12_NC`, +/// whose displacement is encoded pre-shifted by 3 — so anything less than 8-byte alignment cannot +/// be represented and the link fails. +const COMM_ALIGN_BYTES: usize = 8; +const COMM_ALIGN_LOG2: usize = 3; + +/// Renders `.comm`'s third operand for `target`'s object format. +/// +/// Mach-O's assembler documents the operand as `log2(alignment)`; GNU as on ELF documents it as +/// the alignment in bytes. The same intended 8-byte alignment is therefore spelled `3` on Mach-O +/// and `8` on ELF. +fn comm_alignment_operand(target: Target) -> usize { + match target.platform { + Platform::MacOS => COMM_ALIGN_LOG2, + Platform::Linux | Platform::Windows => COMM_ALIGN_BYTES, + } +} + +/// Renders one complete `.comm` directive line, alignment included, for `target`. +/// +/// Every common symbol in the program must go through here rather than spelling the directive +/// inline: the alignment operand is the one part of it that is not portable, and a hardcoded +/// spelling is accepted by both assemblers while only being right for one of them. +pub(crate) fn comm_directive(label: &str, size: usize, target: Target) -> String { + format!( + ".comm {}, {}, {}\n", + label, + size, + comm_alignment_operand(target) + ) +} + #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum DataWord { U64(u64), @@ -151,7 +191,10 @@ impl DataSection { /// Serializes all entries into a GNU assembly `.data` section string. /// Returns an empty string when no entries have been collected. /// Emits `.comm` directives first, then `.ascii` string literals, then `.p2align 3`/`quad` float entries. - pub fn emit(&self) -> String { + /// + /// `target` is required because `.comm`'s alignment operand is spelled differently per object + /// format; see [`comm_alignment_operand`]. + pub fn emit(&self, target: Target) -> String { if self.entries.is_empty() && self.float_entries.is_empty() && self.word_entries.is_empty() @@ -161,8 +204,9 @@ impl DataSection { } let mut out = String::from(".data\n"); + let comm_align = comm_alignment_operand(target); for (label, size) in &self.comm_entries { - out.push_str(&format!(".comm {}, {}, 3\n", label, size)); + out.push_str(&format!(".comm {}, {}, {}\n", label, size, comm_align)); } for (label, bytes) in &self.entries { out.push_str(&format!(".globl {}\n{}:\n", label, label)); @@ -202,6 +246,23 @@ impl DataSection { #[cfg(test)] mod tests { use super::DataSection; + use crate::codegen_support::platform::{Arch, Platform, Target}; + + /// A Mach-O target, whose assembler reads `.comm`'s alignment operand as `log2(bytes)`. + fn macos() -> Target { + Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + } + } + + /// An ELF target, whose assembler reads `.comm`'s alignment operand as a byte count. + fn linux(arch: Arch) -> Target { + Target { + platform: Platform::Linux, + arch, + } + } /// Verifies that float constants use power of two alignment directive. #[test] @@ -209,7 +270,7 @@ mod tests { let mut data = DataSection::new(); data.add_float(3.14); - let asm = data.emit(); + let asm = data.emit(macos()); assert!(asm.contains(".p2align 3\n")); assert!(!asm.contains(".align 3\n")); @@ -221,7 +282,7 @@ mod tests { let mut data = DataSection::new(); data.add_string(b"a\0b"); - let asm = data.emit(); + let asm = data.emit(macos()); assert!(asm.contains(r#".ascii "a\000b""#)); assert!(!asm.contains(r#"\x00b"#)); @@ -236,10 +297,32 @@ mod tests { super::DataWord::Symbol("_fn_demo".to_string()), ]); - let asm = data.emit(); + let asm = data.emit(macos()); assert!(asm.contains(&format!(".globl {}\n{}:\n", label, label))); assert!(asm.contains(" .quad 0x0000000000000001\n")); assert!(asm.contains(" .quad _fn_demo\n")); } + + /// Verifies `.comm` asks each object format for the same 8-byte alignment in the spelling + /// that format's assembler understands: `log2` on Mach-O, bytes on ELF. + /// + /// Emitting the Mach-O spelling on ELF declares 3-byte alignment, which the assembler + /// accepts and the linker then rejects — `R_AARCH64_LDST64_ABS_LO12_NC` encodes its + /// displacement pre-shifted by 3, so a 64-bit load of an under-aligned common symbol fails + /// with `relocation truncated to fit`. That took out every linux-aarch64 link once + /// `_stack_limit` became a common symbol. + #[test] + fn test_comm_alignment_operand_follows_the_object_format() { + let mut data = DataSection::new(); + data.add_comm("_stack_limit".to_string(), 8); + + assert!(data.emit(macos()).contains(".comm _stack_limit, 8, 3\n")); + assert!(data + .emit(linux(Arch::AArch64)) + .contains(".comm _stack_limit, 8, 8\n")); + assert!(data + .emit(linux(Arch::X86_64)) + .contains(".comm _stack_limit, 8, 8\n")); + } } diff --git a/src/codegen_support/dynamic_new.rs b/src/codegen_support/dynamic_new.rs index 2ac568f7ef..70fdc48e90 100644 --- a/src/codegen_support/dynamic_new.rs +++ b/src/codegen_support/dynamic_new.rs @@ -4,10 +4,15 @@ //! //! Called from: //! - `crate::codegen_support::collect_dynamic_object_factory_classes_in_expr()`. +//! - `crate::codegen::lower_inst::objects` candidate selection. +//! - `crate::ir_lower::expr` dynamic-`new` argument planning. //! //! Key details: //! - These classes have known allocation/runtime layouts and can be included in //! emitted class metadata for `new $name` factory paths. +//! - `known_dynamic_new_builtin_class_names()` is the shared "not a user class" +//! filter: EIR lowering must not synthesize fixed-class construction for these +//! names because their allocation is runtime-managed, not AOT-emitted. /// Returns builtin class names with allocation paths that are safe for dynamic `new`. pub(crate) fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { @@ -50,3 +55,89 @@ pub(crate) fn supported_dynamic_new_builtin_class_names() -> &'static [&'static "ValueError", ] } + +/// Returns builtin class names that must not be mistaken for user-instantiable classes. +pub(crate) fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { + &[ + "AppendIterator", + "ArgumentCountError", + "ArrayIterator", + "ArrayObject", + "AssertionError", + "BadFunctionCallException", + "BadMethodCallException", + "CachingIterator", + "CallbackFilterIterator", + "DirectoryIterator", + "DivisionByZeroError", + "DomainException", + "EmptyIterator", + "Error", + "Exception", + "Fiber", + "FiberError", + "FilesystemIterator", + "FilterIterator", + "Generator", + "GlobIterator", + "InfiniteIterator", + "InternalIterator", + "InvalidArgumentException", + "IteratorIterator", + "JsonException", + "LengthException", + "LimitIterator", + "LogicException", + "MultipleIterator", + "NoRewindIterator", + "OutOfBoundsException", + "OutOfRangeException", + "OverflowException", + "ParentIterator", + "Phar", + "PharData", + "RangeException", + "RecursiveArrayIterator", + "RecursiveCachingIterator", + "RecursiveCallbackFilterIterator", + "RecursiveDirectoryIterator", + "RecursiveFilterIterator", + "RecursiveIteratorIterator", + "RecursiveRegexIterator", + "ReflectionAttribute", + "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", + "ReflectionException", + "ReflectionFunction", + "ReflectionMethod", + "ReflectionNamedType", + "ReflectionParameter", + "ReflectionProperty", + "ReflectionUnionType", + "ReflectionIntersectionType", + "RegexIterator", + "RuntimeException", + "SplDoublyLinkedList", + "SplFileInfo", + "SplFileObject", + "SplFixedArray", + "SplHeap", + "SplMaxHeap", + "SplMinHeap", + "SplObjectStorage", + "SplPriorityQueue", + "SplQueue", + "SplStack", + "SplTempFileObject", + "TypeError", + "UnderflowException", + "UnexpectedValueException", + "ValueError", + "ArithmeticError", + "stdClass", + ] +} diff --git a/src/codegen_support/prescan.rs b/src/codegen_support/prescan.rs index dc8b70b325..2368420043 100644 --- a/src/codegen_support/prescan.rs +++ b/src/codegen_support/prescan.rs @@ -17,9 +17,11 @@ use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::ent_constants::ENT_INT_CONSTANTS; use crate::types::error_constants::ERROR_LEVEL_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; +use crate::types::math_constants::MATH_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; use crate::types::session_constants::SESSION_INT_CONSTANTS; use crate::types::stream_constants::STREAM_INT_CONSTANTS; +use crate::types::string_constants::STRING_INT_CONSTANTS; use crate::types::PhpType; /// Seeds the constant map with built-in PHP constants and user-defined constants. @@ -205,12 +207,24 @@ pub(crate) fn collect_constants( (ExprKind::IntLiteral(*value), PhpType::Int), ); } + for (name, value) in STRING_INT_CONSTANTS { + constants.insert( + (*name).to_string(), + (ExprKind::IntLiteral(*value), PhpType::Int), + ); + } for (name, value) in JSON_INT_CONSTANTS { constants.insert( (*name).to_string(), (ExprKind::IntLiteral(*value), PhpType::Int), ); } + for (name, value) in MATH_INT_CONSTANTS { + constants.insert( + (*name).to_string(), + (ExprKind::IntLiteral(*value), PhpType::Int), + ); + } for (name, value) in STREAM_INT_CONSTANTS { constants.insert( (*name).to_string(), diff --git a/src/codegen_support/runtime/arrays/array_chunk_to_hash.rs b/src/codegen_support/runtime/arrays/array_chunk_to_hash.rs new file mode 100644 index 0000000000..ffe7daca34 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_chunk_to_hash.rs @@ -0,0 +1,122 @@ +//! Purpose: +//! Emits the `__rt_array_chunk_to_hash` runtime helper backing `array_chunk($a, $n, true)`. +//! Splits an indexed array into an outer indexed array of owned hashes, each keeping the source +//! integer keys of its own window instead of renumbering them from zero. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - Each chunk is built by `__rt_array_slice_to_hash`, so the window arithmetic, the string +//! persistence and the heap retains are shared with `array_slice($a, $o, $l, true)` instead of +//! being reimplemented. The final chunk is short whenever the source length is not a multiple of +//! the requested size, which the slice helper's clamp already handles. +//! - The outer array holds pointer-sized hash payloads; its `value_type` is stamped by the +//! backend after the helper returns, exactly like the scalar and refcounted chunk helpers. +//! - The chunk count is `ceil(length / size)`, computed the same way as `__rt_array_chunk`, so a +//! chunk size of zero is rejected before the call rather than divided by here. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// array_chunk_to_hash: split an indexed array into key-preserving hash chunks. +/// Input: x0 = source indexed array pointer, x1 = chunk size (must be >= 1) +/// Output: x0 = outer indexed array whose elements are owned hash pointers +/// +/// Backs `array_chunk($array, $length, preserve_keys: true)`. +pub fn emit_array_chunk_to_hash(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_chunk_to_hash_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_chunk_to_hash ---"); + emitter.label_global("__rt_array_chunk_to_hash"); + emitter.instruction("sub sp, sp, #64"); // allocate the chunking stack frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up the new frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the source indexed array pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested chunk size + emitter.instruction("ldr x2, [x0]"); // load the source indexed-array logical length + emitter.instruction("sub x3, x1, #1"); // bias the numerator by chunk_size - 1 + emitter.instruction("add x2, x2, x3"); // length + chunk_size - 1 for ceiling division + emitter.instruction("udiv x2, x2, x1"); // number of chunks = ceil(length / chunk_size) + emitter.instruction("mov x0, x2"); // outer array capacity = number of chunks + emitter.instruction("mov x1, #8"); // outer slots hold pointer-sized hash payloads + emitter.instruction("bl __rt_array_new"); // allocate the outer indexed array, x0 = outer + emitter.instruction("str x0, [sp, #16]"); // save the outer indexed array pointer + emitter.instruction("str xzr, [sp, #24]"); // window cursor i = 0 + emitter.label("__rt_array_chunk_to_hash_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the source indexed array pointer + emitter.instruction("ldr x3, [x0]"); // reload the source logical length + emitter.instruction("ldr x4, [sp, #24]"); // reload the window cursor + emitter.instruction("cmp x4, x3"); // has every source element been assigned to a chunk? + emitter.instruction("b.ge __rt_array_chunk_to_hash_done"); // finish once the source is exhausted + emitter.instruction("mov x1, x4"); // slice offset = current window cursor + emitter.instruction("ldr x2, [sp, #8]"); // slice length = requested chunk size + emitter.instruction("mov x3, #1"); // the chunk length is always explicitly present + emitter.instruction("bl __rt_array_slice_to_hash"); // build this chunk as a key-preserving hash, x0 = chunk + emitter.instruction("mov x1, x0"); // the chunk pointer is the value appended to the outer array + emitter.instruction("ldr x0, [sp, #16]"); // reload the outer indexed array pointer + emitter.instruction("bl __rt_array_push_int"); // append the finished chunk to the outer array + emitter.instruction("str x0, [sp, #16]"); // publish the possibly-grown outer array pointer + emitter.instruction("ldr x4, [sp, #24]"); // reload the window cursor + emitter.instruction("ldr x5, [sp, #8]"); // reload the requested chunk size + emitter.instruction("add x4, x4, x5"); // advance the cursor to the next window + emitter.instruction("str x4, [sp, #24]"); // save the advanced cursor + emitter.instruction("b __rt_array_chunk_to_hash_loop"); // continue with the next chunk + emitter.label("__rt_array_chunk_to_hash_done"); + emitter.instruction("ldr x0, [sp, #16]"); // x0 = outer indexed array pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the stack frame + emitter.instruction("ret"); // return the outer array in x0 +} + +/// x86_64 Linux implementation of `__rt_array_chunk_to_hash`. +/// Input: rdi = source indexed array pointer, rsi = chunk size (must be >= 1) +/// Output: rax = outer indexed array whose elements are owned hash pointers +fn emit_array_chunk_to_hash_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_chunk_to_hash ---"); + emitter.label_global("__rt_array_chunk_to_hash"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("sub rsp, 32"); // reserve local slots for the chunking loop state + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source indexed array pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested chunk size + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the source indexed-array logical length + emitter.instruction("mov rcx, rsi"); // copy the chunk size before biasing the numerator + emitter.instruction("sub rcx, 1"); // bias the numerator by chunk_size - 1 + emitter.instruction("add rax, rcx"); // length + chunk_size - 1 for ceiling division + emitter.instruction("xor edx, edx"); // clear the high dividend half before dividing + emitter.instruction("div rsi"); // number of chunks = ceil(length / chunk_size) + emitter.instruction("mov rdi, rax"); // outer array capacity = number of chunks + emitter.instruction("mov rsi, 8"); // outer slots hold pointer-sized hash payloads + emitter.instruction("call __rt_array_new"); // allocate the outer indexed array, rax = outer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the outer indexed array pointer + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // window cursor i = 0 + emitter.label("__rt_array_chunk_to_hash_loop"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the window cursor + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source indexed array pointer + emitter.instruction("cmp rcx, QWORD PTR [r10]"); // has every source element been assigned to a chunk? + emitter.instruction("jge __rt_array_chunk_to_hash_done"); // finish once the source is exhausted + emitter.instruction("mov rdi, r10"); // slice receiver = the source indexed array + emitter.instruction("mov rsi, rcx"); // slice offset = current window cursor + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // slice length = requested chunk size + emitter.instruction("mov rcx, 1"); // the chunk length is always explicitly present + emitter.instruction("call __rt_array_slice_to_hash"); // build this chunk as a key-preserving hash, rax = chunk + emitter.instruction("mov rsi, rax"); // the chunk pointer is the value appended to the outer array + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the outer indexed array pointer + emitter.instruction("call __rt_array_push_int"); // append the finished chunk to the outer array + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // publish the possibly-grown outer array pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the window cursor + emitter.instruction("add rcx, QWORD PTR [rbp - 16]"); // advance the cursor to the next window + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the advanced cursor + emitter.instruction("jmp __rt_array_chunk_to_hash_loop"); // continue with the next chunk + emitter.label("__rt_array_chunk_to_hash_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // rax = outer indexed array pointer + emitter.instruction("add rsp, 32"); // release the local slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the outer array in rax +} diff --git a/src/codegen_support/runtime/arrays/array_count_values.rs b/src/codegen_support/runtime/arrays/array_count_values.rs new file mode 100644 index 0000000000..e1fbefaab5 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_count_values.rs @@ -0,0 +1,497 @@ +//! Purpose: +//! Emits the `array_count_values()` runtime helpers: `__rt_count_values_bump`, +//! `__rt_array_count_values` (indexed sources) and `__rt_hash_count_values` (associative +//! sources). +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! - `crate::codegen::lower_inst::builtins::arrays::lower_array_count_values()`. +//! +//! Key details: +//! - The destination is always a hash whose `value_type` is `0` (int): php-src's +//! `array_count_values()` maps every distinct value to an `int` occurrence count. +//! - Keys go through `__rt_hash_normalize_key`, so PHP's numeric-string collapsing applies +//! exactly as it does for `$a[$v]` (`array_count_values(["1", 1])` yields `[1 => 2]`). +//! - php-src warns and SKIPS any element that is neither int nor string; the `skip` arms +//! reproduce that through `__rt_diag_warning` with `ARRAY_COUNT_VALUES_SKIPPED_MESSAGES`. +//! - OWNERSHIP: the source is only ever READ. `__rt_hash_set` persists the inserted string key +//! itself and the stored value is a plain integer, so no refcount traffic crosses this helper. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// The `array_count_values()` warning text for a value PHP refuses to use as an array key. +/// +/// Entries are `(symbol, message)`. `crate::codegen_support::runtime::data::fixed` emits them +/// verbatim as `.ascii` literals; this module derives the `write()` length from `message.len()` +/// so the bytes and the immediate can never drift apart. +/// +/// Captured from PHP 8.4.20 with `LC_ALL=C php`; elephc does not synthesize the +/// ` in on line ` tail that php-src appends to the message. +pub const ARRAY_COUNT_VALUES_SKIPPED_MESSAGES: &[(&str, &str)] = &[( + "_diag_array_count_values_skipped", + "Warning: array_count_values(): Can only count string and integer values, entry skipped\n", +)]; + +/// Returns the byte length of the single `array_count_values()` skip message. +fn skip_message_len() -> usize { + ARRAY_COUNT_VALUES_SKIPPED_MESSAGES[0].1.len() +} + +/// Emits every `array_count_values()` runtime helper for the active target. +pub fn emit_array_count_values(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_count_values_bump_x86_64(emitter); + emit_array_count_values_x86_64(emitter); + emit_hash_count_values_x86_64(emitter); + return; + } + emit_count_values_bump_aarch64(emitter); + emit_array_count_values_aarch64(emitter); + emit_hash_count_values_aarch64(emitter); +} + +/// Emits `__rt_count_values_bump`, the shared "increment the tally for one key" routine. +/// +/// # ABI (AArch64) +/// - Input: `x0` = destination hash, `x1` = normalized key_lo, `x2` = normalized key_hi. +/// - Output: `x0` = destination hash (possibly reallocated by `__rt_hash_set`). +fn emit_count_values_bump_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: count_values_bump ---"); + emitter.label_global("__rt_count_values_bump"); + + // Stack layout: + // [sp, #0] = destination hash pointer + // [sp, #8] = normalized key_lo + // [sp, #16] = normalized key_hi + // [sp, #32] = saved x29/x30 + emitter.instruction("sub sp, sp, #48"); // allocate the tally-bump frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // set up the tally-bump frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination hash across the lookup + emitter.instruction("str x1, [sp, #8]"); // preserve the normalized key low word + emitter.instruction("str x2, [sp, #16]"); // preserve the normalized key high word + + emitter.instruction("bl __rt_hash_get"); // x0 = found, x1 = value_lo (the previous tally) + emitter.instruction("mov x9, #1"); // a value seen for the first time starts at 1 + emitter.instruction("add x10, x1, #1"); // an already-tallied value grows by one + emitter.instruction("cmp x0, #0"); // did the destination already hold this key? + emitter.instruction("csel x3, x10, x9, ne"); // pick the incremented tally only for an existing key + + emitter.instruction("ldr x0, [sp, #0]"); // x0 = destination hash pointer + emitter.instruction("ldr x1, [sp, #8]"); // x1 = normalized key low word + emitter.instruction("ldr x2, [sp, #16]"); // x2 = normalized key high word + emitter.instruction("mov x4, xzr"); // integer tallies carry no high word + emitter.instruction("mov x5, xzr"); // runtime tag 0 marks the tally as an int + emitter.instruction("bl __rt_hash_set"); // insert or overwrite the tally; hash_set persists string keys + + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // deallocate the tally-bump frame + emitter.instruction("ret"); // return with x0 = destination hash pointer +} + +/// Emits `__rt_array_count_values` for INDEXED array sources. +/// +/// # ABI (AArch64) +/// - Input: `x0` = source indexed array, `x1` = compile-time element tag +/// (`0` int, `1` string, `7` boxed Mixed; any other tag makes every element skippable). +/// - Output: `x0` = fresh destination hash mapping each countable value to its tally. +fn emit_array_count_values_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_count_values ---"); + emitter.label_global("__rt_array_count_values"); + + // Stack layout: + // [sp, #0] = source array pointer + // [sp, #8] = destination hash pointer + // [sp, #16] = loop index + // [sp, #24] = element tag + // [sp, #48] = saved x29/x30 + emitter.instruction("sub sp, sp, #64"); // allocate the count-values frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up the count-values frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the source array across helper calls + emitter.instruction("str x1, [sp, #24]"); // preserve the compile-time element tag + + emitter.instruction("ldr x0, [x0]"); // x0 = source element count + emitter.instruction("lsl x0, x0, #1"); // double it so the destination has insertion headroom + emitter.instruction("mov x9, #16"); // x9 = minimum destination bucket count + emitter.instruction("cmp x0, x9"); // compare the derived capacity against the runtime minimum + emitter.instruction("csel x0, x9, x0, lt"); // clamp very small sources up to the minimum bucket count + emitter.instruction("mov x1, xzr"); // destination value_type 0: tallies are integers + emitter.instruction("bl __rt_hash_new"); // allocate the destination hash + emitter.instruction("str x0, [sp, #8]"); // preserve the destination hash across insertions + emitter.instruction("str xzr, [sp, #16]"); // start the walk at source index 0 + + emitter.label("__rt_array_count_values_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // x0 = source array pointer + emitter.instruction("ldr x9, [x0]"); // x9 = source element count + emitter.instruction("ldr x10, [sp, #16]"); // x10 = current source index + emitter.instruction("cmp x10, x9"); // has every source element been tallied? + emitter.instruction("b.ge __rt_array_count_values_done"); // yes - the destination hash is complete + emitter.instruction("add x11, x0, #24"); // x11 = source payload base + emitter.instruction("ldr x12, [sp, #24]"); // x12 = compile-time element tag + emitter.instruction("cmp x12, #1"); // element tag 1 = string payload + emitter.instruction("b.eq __rt_array_count_values_str"); // string elements live in 16-byte slots + emitter.instruction("cmp x12, #7"); // element tag 7 = boxed Mixed payload + emitter.instruction("b.eq __rt_array_count_values_mixed"); // boxed elements need a runtime tag dispatch + emitter.instruction("cmp x12, #0"); // element tag 0 = plain integer payload + emitter.instruction("b.ne __rt_array_count_values_skip"); // float/bool/array/object elements are skipped + emitter.instruction("ldr x1, [x11, x10, lsl #3]"); // x1 = integer value becoming the tally key + emitter.instruction("mov x2, #-1"); // key_hi sentinel marks an inline integer key + emitter.instruction("b __rt_array_count_values_bump"); // tally this integer value + + emitter.label("__rt_array_count_values_str"); + emitter.instruction("add x11, x11, x10, lsl #4"); // advance to the selected 16-byte string slot + emitter.instruction("ldr x1, [x11]"); // x1 = source string pointer + emitter.instruction("ldr x2, [x11, #8]"); // x2 = source string length + emitter.instruction("bl __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("b __rt_array_count_values_bump"); // tally this string value + + emitter.label("__rt_array_count_values_mixed"); + emitter.instruction("ldr x0, [x11, x10, lsl #3]"); // x0 = boxed Mixed cell for this element + emitter.instruction("cbz x0, __rt_array_count_values_skip"); // a null cell is not a countable value + emitter.instruction("bl __rt_mixed_unbox"); // x0 = concrete tag, x1 = value_lo, x2 = value_hi + emitter.instruction("cmp x0, #0"); // runtime tag 0 = int + emitter.instruction("b.eq __rt_array_count_values_mixed_int"); // integers become inline integer keys + emitter.instruction("cmp x0, #1"); // runtime tag 1 = string + emitter.instruction("b.ne __rt_array_count_values_skip"); // every other tag is skipped with a warning + emitter.instruction("bl __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("b __rt_array_count_values_bump"); // tally this unboxed string value + + emitter.label("__rt_array_count_values_mixed_int"); + emitter.instruction("mov x2, #-1"); // key_hi sentinel marks an inline integer key + + emitter.label("__rt_array_count_values_bump"); + emitter.instruction("ldr x0, [sp, #8]"); // x0 = destination hash pointer + emitter.instruction("bl __rt_count_values_bump"); // increment the tally for this key + emitter.instruction("str x0, [sp, #8]"); // keep the destination pointer current after growth + + emitter.label("__rt_array_count_values_next"); + emitter.instruction("ldr x10, [sp, #16]"); // reload the source index after the helper calls + emitter.instruction("add x10, x10, #1"); // advance to the next source element + emitter.instruction("str x10, [sp, #16]"); // persist the updated source index + emitter.instruction("b __rt_array_count_values_loop"); // continue tallying source elements + + emitter.label("__rt_array_count_values_skip"); + abi::emit_symbol_address(emitter, "x1", ARRAY_COUNT_VALUES_SKIPPED_MESSAGES[0].0); + emitter.instruction(&format!("mov x2, #{}", skip_message_len())); // pass the complete skip-warning length + emitter.instruction("bl __rt_diag_warning"); // emit or suppress the PHP skip warning + emitter.instruction("b __rt_array_count_values_next"); // skip this element and continue + + emitter.label("__rt_array_count_values_done"); + emitter.instruction("ldr x0, [sp, #8]"); // return the destination hash pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the count-values frame + emitter.instruction("ret"); // return with x0 = destination hash pointer +} + +/// Emits `__rt_hash_count_values` for ASSOCIATIVE array sources. +/// +/// # ABI (AArch64) +/// - Input: `x0` = source hash. +/// - Output: `x0` = fresh destination hash mapping each countable value to its tally. +/// +/// Hash entries carry a per-entry runtime tag, so one routine covers `Int`, `Str`, and boxed +/// `Mixed` value types without a compile-time hint. +fn emit_hash_count_values_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_count_values ---"); + emitter.label_global("__rt_hash_count_values"); + + // Stack layout: + // [sp, #0] = source hash pointer + // [sp, #8] = destination hash pointer + // [sp, #16] = insertion-order iterator cursor + // [sp, #48] = saved x29/x30 + emitter.instruction("sub sp, sp, #64"); // allocate the hash count-values frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up the hash count-values frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the source hash across helper calls + + emitter.instruction("ldr x0, [x0]"); // x0 = source entry count + emitter.instruction("lsl x0, x0, #1"); // double it so the destination has insertion headroom + emitter.instruction("mov x9, #16"); // x9 = minimum destination bucket count + emitter.instruction("cmp x0, x9"); // compare the derived capacity against the runtime minimum + emitter.instruction("csel x0, x9, x0, lt"); // clamp very small sources up to the minimum bucket count + emitter.instruction("mov x1, xzr"); // destination value_type 0: tallies are integers + emitter.instruction("bl __rt_hash_new"); // allocate the destination hash + emitter.instruction("str x0, [sp, #8]"); // preserve the destination hash across insertions + emitter.instruction("str xzr, [sp, #16]"); // iterator cursor = 0 (start from header.head) + + emitter.label("__rt_hash_count_values_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // x0 = source hash pointer + emitter.instruction("ldr x1, [sp, #16]"); // x1 = current insertion-order cursor + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next source entry + emitter.instruction("cmn x0, #1"); // did the iterator signal end-of-walk? + emitter.instruction("b.eq __rt_hash_count_values_done"); // yes - the destination hash is complete + emitter.instruction("str x0, [sp, #16]"); // save the next insertion-order cursor + emitter.instruction("mov x0, x5"); // x0 = source value tag + emitter.instruction("mov x1, x3"); // x1 = source value low word + emitter.instruction("mov x2, x4"); // x2 = source value high word + emitter.instruction("cmp x0, #7"); // runtime tag 7 = boxed mixed cell + emitter.instruction("b.ne __rt_hash_count_values_tag_ready"); // concrete tags are already usable + emitter.instruction("cbz x1, __rt_hash_count_values_skip"); // a null cell is not a countable value + emitter.instruction("mov x0, x1"); // x0 = boxed mixed pointer for unboxing + emitter.instruction("bl __rt_mixed_unbox"); // x0 = concrete tag, x1 = value_lo, x2 = value_hi + + emitter.label("__rt_hash_count_values_tag_ready"); + emitter.instruction("cmp x0, #0"); // runtime tag 0 = int + emitter.instruction("b.eq __rt_hash_count_values_int"); // integers become inline integer keys + emitter.instruction("cmp x0, #1"); // runtime tag 1 = string + emitter.instruction("b.ne __rt_hash_count_values_skip"); // every other tag is skipped with a warning + emitter.instruction("bl __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("b __rt_hash_count_values_bump"); // tally this string value + + emitter.label("__rt_hash_count_values_int"); + emitter.instruction("mov x2, #-1"); // key_hi sentinel marks an inline integer key + + emitter.label("__rt_hash_count_values_bump"); + emitter.instruction("ldr x0, [sp, #8]"); // x0 = destination hash pointer + emitter.instruction("bl __rt_count_values_bump"); // increment the tally for this key + emitter.instruction("str x0, [sp, #8]"); // keep the destination pointer current after growth + emitter.instruction("b __rt_hash_count_values_loop"); // continue with the next source entry + + emitter.label("__rt_hash_count_values_skip"); + abi::emit_symbol_address(emitter, "x1", ARRAY_COUNT_VALUES_SKIPPED_MESSAGES[0].0); + emitter.instruction(&format!("mov x2, #{}", skip_message_len())); // pass the complete skip-warning length + emitter.instruction("bl __rt_diag_warning"); // emit or suppress the PHP skip warning + emitter.instruction("b __rt_hash_count_values_loop"); // skip this entry and continue + + emitter.label("__rt_hash_count_values_done"); + emitter.instruction("ldr x0, [sp, #8]"); // return the destination hash pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the hash count-values frame + emitter.instruction("ret"); // return with x0 = destination hash pointer +} + +/// Emits the x86_64 System V variant of `__rt_count_values_bump`. +/// +/// `__rt_hash_get` takes `(rdi = hash, rsi = key_lo, rdx = key_hi)` and returns +/// `rax = found`, `rdi = value_lo`; every field is spilled before the insertion call. +fn emit_count_values_bump_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: count_values_bump ---"); + emitter.label_global("__rt_count_values_bump"); + + // Frame layout: + // [rbp - 8] = destination hash pointer + // [rbp - 16] = normalized key_lo + // [rbp - 24] = normalized key_hi + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the tally bookkeeping + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the nested helper calls + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination hash across the lookup + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the normalized key high word + + emitter.instruction("call __rt_hash_get"); // rax = found, rdi = value_lo (the previous tally) + emitter.instruction("lea rcx, [rdi + 1]"); // an already-tallied value grows by one + emitter.instruction("mov r10, 1"); // a value seen for the first time starts at 1 + emitter.instruction("test rax, rax"); // did the destination already hold this key? + emitter.instruction("cmovne r10, rcx"); // pick the incremented tally only for an existing key + + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // rdi = destination hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // rsi = normalized key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // rdx = normalized key high word + emitter.instruction("mov rcx, r10"); // rcx = the tally being stored + emitter.instruction("xor r8d, r8d"); // integer tallies carry no high word + emitter.instruction("xor r9d, r9d"); // runtime tag 0 marks the tally as an int + emitter.instruction("call __rt_hash_set"); // insert or overwrite the tally; hash_set persists string keys + + emitter.instruction("add rsp, 32"); // release the tally-bump spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with rax = destination hash pointer +} + +/// Emits the x86_64 System V variant of `__rt_array_count_values`. +/// +/// Mirrors the AArch64 logic; `__rt_hash_normalize_key` and `__rt_mixed_unbox` use the +/// `rax`/`rdx` convention rather than the System V argument registers, exactly as +/// `__rt_hash_flip` calls them. +fn emit_array_count_values_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_count_values ---"); + emitter.label_global("__rt_array_count_values"); + + // Frame layout: + // [rbp - 8] = source array pointer + // [rbp - 16] = destination hash pointer + // [rbp - 24] = loop index + // [rbp - 32] = element tag + // [rbp - 40] = normalized key_lo + // [rbp - 48] = normalized key_hi + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the walk bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the nested helper calls + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source array across helper calls + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // preserve the compile-time element tag + + emitter.instruction("mov rax, QWORD PTR [rdi]"); // rax = source element count + emitter.instruction("shl rax, 1"); // double it so the destination has insertion headroom + emitter.instruction("cmp rax, 16"); // compare the derived capacity against the runtime minimum + emitter.instruction("jge __rt_array_count_values_capacity_x86"); // keep the doubled count when it already meets the minimum + emitter.instruction("mov rax, 16"); // clamp very small sources up to the minimum bucket count + emitter.label("__rt_array_count_values_capacity_x86"); + emitter.instruction("mov rdi, rax"); // rdi = destination bucket count + emitter.instruction("xor esi, esi"); // destination value_type 0: tallies are integers + emitter.instruction("call __rt_hash_new"); // allocate the destination hash + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the destination hash across insertions + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // start the walk at source index 0 + + emitter.label("__rt_array_count_values_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // r10 = source array pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // rcx = current source index + emitter.instruction("cmp rcx, QWORD PTR [r10]"); // has every source element been tallied? + emitter.instruction("jge __rt_array_count_values_done_x86"); // yes - the destination hash is complete + emitter.instruction("lea r11, [r10 + 24]"); // r11 = source payload base + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // r10 = compile-time element tag + emitter.instruction("cmp r10, 1"); // element tag 1 = string payload + emitter.instruction("je __rt_array_count_values_str_x86"); // string elements live in 16-byte slots + emitter.instruction("cmp r10, 7"); // element tag 7 = boxed Mixed payload + emitter.instruction("je __rt_array_count_values_mixed_x86"); // boxed elements need a runtime tag dispatch + emitter.instruction("cmp r10, 0"); // element tag 0 = plain integer payload + emitter.instruction("jne __rt_array_count_values_skip_x86"); // float/bool/array/object elements are skipped + emitter.instruction("mov rax, QWORD PTR [r11 + rcx * 8]"); // rax = integer value becoming the tally key + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the tally key low word + emitter.instruction("mov QWORD PTR [rbp - 48], -1"); // key_hi sentinel marks an inline integer key + emitter.instruction("jmp __rt_array_count_values_bump_x86"); // tally this integer value + + emitter.label("__rt_array_count_values_str_x86"); + emitter.instruction("shl rcx, 4"); // convert the element index into a 16-byte slot offset + emitter.instruction("add r11, rcx"); // advance to the selected string slot + emitter.instruction("mov rax, QWORD PTR [r11]"); // rax = source string pointer + emitter.instruction("mov rdx, QWORD PTR [r11 + 8]"); // rdx = source string length + emitter.instruction("call __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the normalized key high word + emitter.instruction("jmp __rt_array_count_values_bump_x86"); // tally this string value + + emitter.label("__rt_array_count_values_mixed_x86"); + emitter.instruction("mov rax, QWORD PTR [r11 + rcx * 8]"); // rax = boxed Mixed cell for this element + emitter.instruction("test rax, rax"); // is the cell null? + emitter.instruction("je __rt_array_count_values_skip_x86"); // a null cell is not a countable value + emitter.instruction("call __rt_mixed_unbox"); // rax = concrete tag, rdi = value_lo, rdx = value_hi + emitter.instruction("cmp rax, 0"); // runtime tag 0 = int + emitter.instruction("je __rt_array_count_values_mixed_int_x86"); // integers become inline integer keys + emitter.instruction("cmp rax, 1"); // runtime tag 1 = string + emitter.instruction("jne __rt_array_count_values_skip_x86"); // every other tag is skipped with a warning + emitter.instruction("mov rax, rdi"); // rax = unboxed string pointer + emitter.instruction("call __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the normalized key high word + emitter.instruction("jmp __rt_array_count_values_bump_x86"); // tally this unboxed string value + + emitter.label("__rt_array_count_values_mixed_int_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the unboxed integer as the tally key + emitter.instruction("mov QWORD PTR [rbp - 48], -1"); // key_hi sentinel marks an inline integer key + + emitter.label("__rt_array_count_values_bump_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // rdi = destination hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // rsi = tally key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // rdx = tally key high word + emitter.instruction("call __rt_count_values_bump"); // increment the tally for this key + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // keep the destination pointer current after growth + + emitter.label("__rt_array_count_values_next_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the source index after the helper calls + emitter.instruction("add r10, 1"); // advance to the next source element + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // persist the updated source index + emitter.instruction("jmp __rt_array_count_values_loop_x86"); // continue tallying source elements + + emitter.label("__rt_array_count_values_skip_x86"); + abi::emit_symbol_address(emitter, "rdi", ARRAY_COUNT_VALUES_SKIPPED_MESSAGES[0].0); + emitter.instruction(&format!("mov esi, {}", skip_message_len())); // pass the complete skip-warning length + emitter.instruction("call __rt_diag_warning"); // emit or suppress the PHP skip warning + emitter.instruction("jmp __rt_array_count_values_next_x86"); // skip this element and continue + + emitter.label("__rt_array_count_values_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return the destination hash pointer + emitter.instruction("add rsp, 64"); // release the count-values spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with rax = destination hash pointer +} + +/// Emits the x86_64 System V variant of `__rt_hash_count_values`. +fn emit_hash_count_values_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_count_values ---"); + emitter.label_global("__rt_hash_count_values"); + + // Frame layout: + // [rbp - 8] = source hash pointer + // [rbp - 16] = destination hash pointer + // [rbp - 24] = insertion-order iterator cursor + // [rbp - 32] = normalized key_lo + // [rbp - 40] = normalized key_hi + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the walk bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the nested helper calls + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source hash across helper calls + + emitter.instruction("mov rax, QWORD PTR [rdi]"); // rax = source entry count + emitter.instruction("shl rax, 1"); // double it so the destination has insertion headroom + emitter.instruction("cmp rax, 16"); // compare the derived capacity against the runtime minimum + emitter.instruction("jge __rt_hash_count_values_capacity_x86"); // keep the doubled count when it already meets the minimum + emitter.instruction("mov rax, 16"); // clamp very small sources up to the minimum bucket count + emitter.label("__rt_hash_count_values_capacity_x86"); + emitter.instruction("mov rdi, rax"); // rdi = destination bucket count + emitter.instruction("xor esi, esi"); // destination value_type 0: tallies are integers + emitter.instruction("call __rt_hash_new"); // allocate the destination hash + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the destination hash across insertions + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // iterator cursor = 0 (start from header.head) + + emitter.label("__rt_hash_count_values_loop_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // rdi = source hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = current insertion-order cursor + emitter.instruction("call __rt_hash_iter_next"); // rax=cursor, rcx=value_lo, r8=value_hi, r9=value_tag + emitter.instruction("cmp rax, -1"); // did the iterator signal end-of-walk? + emitter.instruction("je __rt_hash_count_values_done_x86"); // yes - the destination hash is complete + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the next insertion-order cursor + emitter.instruction("mov rax, r9"); // rax = source value tag + emitter.instruction("mov rdi, rcx"); // rdi = source value low word + emitter.instruction("mov rdx, r8"); // rdx = source value high word + emitter.instruction("cmp rax, 7"); // runtime tag 7 = boxed mixed cell + emitter.instruction("jne __rt_hash_count_values_tag_ready_x86"); // concrete tags are already usable + emitter.instruction("test rdi, rdi"); // is the boxed cell null? + emitter.instruction("je __rt_hash_count_values_skip_x86"); // a null cell is not a countable value + emitter.instruction("mov rax, rdi"); // rax = boxed mixed pointer for unboxing + emitter.instruction("call __rt_mixed_unbox"); // rax = concrete tag, rdi = value_lo, rdx = value_hi + + emitter.label("__rt_hash_count_values_tag_ready_x86"); + emitter.instruction("cmp rax, 0"); // runtime tag 0 = int + emitter.instruction("je __rt_hash_count_values_int_x86"); // integers become inline integer keys + emitter.instruction("cmp rax, 1"); // runtime tag 1 = string + emitter.instruction("jne __rt_hash_count_values_skip_x86"); // every other tag is skipped with a warning + emitter.instruction("mov rax, rdi"); // rax = source string pointer + emitter.instruction("call __rt_hash_normalize_key"); // collapse PHP numeric-string values into integer keys + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the normalized key high word + emitter.instruction("jmp __rt_hash_count_values_bump_x86"); // tally this string value + + emitter.label("__rt_hash_count_values_int_x86"); + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the integer value as the tally key + emitter.instruction("mov QWORD PTR [rbp - 40], -1"); // key_hi sentinel marks an inline integer key + + emitter.label("__rt_hash_count_values_bump_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // rdi = destination hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // rsi = tally key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // rdx = tally key high word + emitter.instruction("call __rt_count_values_bump"); // increment the tally for this key + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // keep the destination pointer current after growth + emitter.instruction("jmp __rt_hash_count_values_loop_x86"); // continue with the next source entry + + emitter.label("__rt_hash_count_values_skip_x86"); + abi::emit_symbol_address(emitter, "rdi", ARRAY_COUNT_VALUES_SKIPPED_MESSAGES[0].0); + emitter.instruction(&format!("mov esi, {}", skip_message_len())); // pass the complete skip-warning length + emitter.instruction("call __rt_diag_warning"); // emit or suppress the PHP skip warning + emitter.instruction("jmp __rt_hash_count_values_loop_x86"); // skip this entry and continue + + emitter.label("__rt_hash_count_values_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // return the destination hash pointer + emitter.instruction("add rsp, 64"); // release the count-values spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with rax = destination hash pointer +} diff --git a/src/codegen_support/runtime/arrays/array_internal_pointer.rs b/src/codegen_support/runtime/arrays/array_internal_pointer.rs new file mode 100644 index 0000000000..01dff510f3 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_internal_pointer.rs @@ -0,0 +1,409 @@ +//! Purpose: +//! Emits the `__rt_array_ptr_seek`, `__rt_array_ptr_key` and `__rt_array_ptr_value` +//! runtime helpers backing PHP's internal array pointer family +//! (`key`, `current`, `next`, `prev`, `reset`, `end`). +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - The cursor is a LOGICAL ORDINAL into the container's iteration order, never a +//! physical bucket index, so every read is bounds-checked against the live element +//! count and a stale cursor can only produce `false`/`null`, never an out-of-bounds +//! read. `-1` is the single canonical "invalid" cursor, matching PHP's one-way +//! past-the-end state (`prev()` off the front and `next()` off the back both land in +//! the same unrecoverable position; only `reset`/`end` restore a valid cursor). +//! - All three helpers are LEAF routines: each starts with an inline normalization loop +//! that unwraps boxed Mixed cells, and every exit is a tail jump. Nothing here builds a +//! stack frame, so nothing can clobber the caller's return address. +//! - After normalization the live element count is at header word 0 for BOTH indexed +//! arrays (kind 2) and hashes (kind 3), so the bounds check is one load either way. +//! - Value boxing is delegated to already-audited ownership paths: indexed storage tail +//! calls `__rt_array_get_mixed_key` (the ordinary `$a[$i]` read path, which understands +//! every indexed `value_type`), hash storage tail calls `__rt_mixed_from_value` (which +//! retains containers and persists strings). +//! - Hash ordinals are resolved by walking the insertion-order `next` chain, so hash +//! reads are `O(cursor)`; indexed reads are `O(1)`. +//! - The seek modes consumed by `__rt_array_ptr_seek` are `0` reset, `1` end, `2` next and +//! `3` prev. `crate::builtins::semantics::ArrayPointerOp::seek_mode` is the single source +//! of truth for that mapping and must stay in step with the dispatch below. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the AArch64 inline normalization prologue shared by all three helpers. +/// +/// Unwraps boxed Mixed cells until a bare container remains, then leaves the container +/// pointer in `x0`, its live element count in `x11`, and its heap kind in `x12`, or +/// branches to `invalid` when the input is not a live array or hash. Only `x9`-`x12` are +/// touched, so the caller's `x1`/`x2` argument registers survive. +fn emit_normalize_aarch64(emitter: &mut Emitter, prefix: &str, invalid: &str) { + emitter.label(&format!("{}_norm", prefix)); + emitter.instruction(&format!("cbz x0, {}", invalid)); // a null container is never positionable + crate::codegen_support::abi::emit_load_int_immediate( + emitter, + "x9", + crate::codegen_support::sentinels::NULL_SENTINEL, + ); + emitter.instruction("cmp x0, x9"); // does the container carry the in-band null sentinel? + emitter.instruction(&format!("b.eq {}", invalid)); // sentinel-null containers are never positionable + emitter.instruction("ldr x9, [x0, #-8]"); // load the uniform heap-kind header word + emitter.instruction("and x9, x9, #0xff"); // isolate the low-byte heap kind + emitter.instruction("cmp x9, #5"); // is the container a boxed mixed cell? + emitter.instruction(&format!("b.eq {}_unbox", prefix)); // mixed cells are unwrapped before anything else + emitter.instruction("cmp x9, #2"); // is the container an indexed array? + emitter.instruction(&format!("b.eq {}_live", prefix)); // indexed arrays keep their count at header word 0 + emitter.instruction("cmp x9, #3"); // is the container an associative hash? + emitter.instruction(&format!("b.eq {}_live", prefix)); // hashes also keep their count at header word 0 + emitter.instruction(&format!("b {}", invalid)); // any other kind is not iterable here + emitter.label(&format!("{}_unbox", prefix)); + emitter.instruction("ldr x10, [x0]"); // load the boxed mixed value tag + emitter.instruction("cmp x10, #4"); // does the cell box an indexed array? + emitter.instruction(&format!("b.eq {}_unwrap", prefix)); // unwrap indexed array payloads + emitter.instruction("cmp x10, #5"); // does the cell box an associative array? + emitter.instruction(&format!("b.ne {}", invalid)); // non-array mixed payloads are never positionable + emitter.label(&format!("{}_unwrap", prefix)); + emitter.instruction("ldr x0, [x0, #8]"); // unbox the container pointer from mixed[8] + emitter.instruction(&format!("b {}_norm", prefix)); // re-normalize in case the payload nests another cell + emitter.label(&format!("{}_live", prefix)); + emitter.instruction("mov x12, x9"); // x12 = heap kind (2 = indexed, 3 = hash) + emitter.instruction("ldr x11, [x0, #0]"); // x11 = live element count from header word 0 +} + +/// Emits the x86_64 inline normalization prologue shared by all three helpers. +/// +/// Unwraps boxed Mixed cells until a bare container remains, then leaves the container +/// pointer in `rdi` and its live element count in `r11`, or branches to `invalid`. Only +/// `rax`, `r10` and `r11` are touched, so the caller's `rsi`/`rdx` argument registers +/// survive; callers that need the heap kind reload the header byte after the check. +fn emit_normalize_x86_64(emitter: &mut Emitter, prefix: &str, invalid: &str) { + emitter.label(&format!("{}_norm", prefix)); + emitter.instruction("test rdi, rdi"); // is the container pointer null? + emitter.instruction(&format!("je {}", invalid)); // a null container is never positionable + crate::codegen_support::abi::emit_load_int_immediate( + emitter, + "r10", + crate::codegen_support::sentinels::NULL_SENTINEL, + ); + emitter.instruction("cmp rdi, r10"); // does the container carry the in-band null sentinel? + emitter.instruction(&format!("je {}", invalid)); // sentinel-null containers are never positionable + emitter.instruction("movzx eax, BYTE PTR [rdi - 8]"); // load the low-byte heap kind from the uniform header + emitter.instruction("cmp eax, 5"); // is the container a boxed mixed cell? + emitter.instruction(&format!("je {}_unbox", prefix)); // mixed cells are unwrapped before anything else + emitter.instruction("cmp eax, 2"); // is the container an indexed array? + emitter.instruction(&format!("je {}_live", prefix)); // indexed arrays keep their count at header word 0 + emitter.instruction("cmp eax, 3"); // is the container an associative hash? + emitter.instruction(&format!("je {}_live", prefix)); // hashes also keep their count at header word 0 + emitter.instruction(&format!("jmp {}", invalid)); // any other kind is not iterable here + emitter.label(&format!("{}_unbox", prefix)); + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed mixed value tag + emitter.instruction("cmp r10, 4"); // does the cell box an indexed array? + emitter.instruction(&format!("je {}_unwrap", prefix)); // unwrap indexed array payloads + emitter.instruction("cmp r10, 5"); // does the cell box an associative array? + emitter.instruction(&format!("jne {}", invalid)); // non-array mixed payloads are never positionable + emitter.label(&format!("{}_unwrap", prefix)); + emitter.instruction("mov rdi, QWORD PTR [rdi + 8]"); // unbox the container pointer from mixed[8] + emitter.instruction(&format!("jmp {}_norm", prefix)); // re-normalize in case the payload nests another cell + emitter.label(&format!("{}_live", prefix)); + emitter.instruction("mov r11, QWORD PTR [rdi]"); // r11 = live element count from header word 0 +} + +/// Emits the AArch64 inline hash ordinal walk. +/// +/// Follows the insertion-order `next` chain from the header head slot `x1` times and +/// leaves the selected entry's address in `x10`. The cursor has already been bounds +/// checked against the live count, so a chain that runs out early means the table is +/// inconsistent; that branches to `invalid` instead of reading past the entries. +fn emit_hash_walk_aarch64(emitter: &mut Emitter, prefix: &str, invalid: &str) { + emitter.instruction("ldr x9, [x0, #24]"); // x9 = insertion-order head slot index + emitter.label(&format!("{}_walk", prefix)); + emitter.instruction("cmn x9, #1"); // has the insertion-order chain run out? + emitter.instruction(&format!("b.eq {}", invalid)); // an exhausted chain has no entry at this ordinal + emitter.instruction("mov x10, #64"); // x10 = hash entry stride in bytes + emitter.instruction("mul x10, x9, x10"); // byte offset of the current slot + emitter.instruction("add x10, x0, x10"); // advance from the hash base to the slot + emitter.instruction("add x10, x10, #40"); // skip the 40-byte hash header + emitter.instruction(&format!("cbz x1, {}_walk_done", prefix)); // ordinal 0 selects the current entry + emitter.instruction("sub x1, x1, #1"); // consume one step of the requested ordinal + emitter.instruction("ldr x9, [x10, #56]"); // x9 = next slot index from the insertion-order chain + emitter.instruction(&format!("b {}_walk", prefix)); // keep walking towards the requested ordinal + emitter.label(&format!("{}_walk_done", prefix)); +} + +/// Emits the x86_64 inline hash ordinal walk. +/// +/// Follows the insertion-order `next` chain from the header head slot `rsi` times and +/// leaves the selected entry's address in `r10`. Branches to `invalid` if the chain runs +/// out before the bounds-checked ordinal is reached. +fn emit_hash_walk_x86_64(emitter: &mut Emitter, prefix: &str, invalid: &str) { + emitter.instruction("mov rax, QWORD PTR [rdi + 24]"); // rax = insertion-order head slot index + emitter.label(&format!("{}_walk", prefix)); + emitter.instruction("cmp rax, -1"); // has the insertion-order chain run out? + emitter.instruction(&format!("je {}", invalid)); // an exhausted chain has no entry at this ordinal + emitter.instruction("mov r10, rax"); // copy the slot index before scaling it + emitter.instruction("shl r10, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add r10, rdi"); // advance from the hash base to the slot + emitter.instruction("add r10, 40"); // skip the 40-byte hash header + emitter.instruction("test rsi, rsi"); // is the requested ordinal exhausted? + emitter.instruction(&format!("je {}_walk_done", prefix)); // ordinal 0 selects the current entry + emitter.instruction("sub rsi, 1"); // consume one step of the requested ordinal + emitter.instruction("mov rax, QWORD PTR [r10 + 56]"); // rax = next slot index from the insertion-order chain + emitter.instruction(&format!("jmp {}_walk", prefix)); // keep walking towards the requested ordinal + emitter.label(&format!("{}_walk_done", prefix)); +} + +/// array_ptr_seek: compute the next internal-pointer cursor for one PHP seek operation. +/// +/// The cursor is a logical ordinal; `-1` is the single canonical invalid position. PHP +/// only ever leaves the invalid position through `reset`/`end`, so `next`/`prev` short out +/// to `-1` whenever the incoming cursor is already out of range — which is what makes +/// `end($a); next($a); prev($a)` report `false` three times instead of walking back in. +/// +/// Input: x0 = container pointer, x1 = current cursor, x2 = seek mode +/// (0 = reset, 1 = end, 2 = next, 3 = prev) +/// Output: x0 = new cursor, or `-1` when the container has no element at that position +pub fn emit_array_ptr_seek(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_ptr_seek_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_ptr_seek ---"); + emitter.label_global("__rt_array_ptr_seek"); + emit_normalize_aarch64(emitter, "__rt_aptr_seek", "__rt_aptr_seek_invalid"); + emitter.instruction("cbz x11, __rt_aptr_seek_invalid"); // an empty container has no valid position at all + emitter.instruction("cmp x2, #0"); // is this a reset (mode 0)? + emitter.instruction("b.eq __rt_aptr_seek_first"); // reset rewinds to the first ordinal + emitter.instruction("cmp x2, #1"); // is this an end (mode 1)? + emitter.instruction("b.eq __rt_aptr_seek_last"); // end jumps to the final ordinal + emitter.instruction("cmp x1, #0"); // is the incoming cursor before the first ordinal? + emitter.instruction("b.lt __rt_aptr_seek_invalid"); // an already-invalid cursor stays invalid + emitter.instruction("cmp x1, x11"); // is the incoming cursor past the last ordinal? + emitter.instruction("b.ge __rt_aptr_seek_invalid"); // a stale past-the-end cursor stays invalid + emitter.instruction("cmp x2, #2"); // is this a next (mode 2)? + emitter.instruction("b.eq __rt_aptr_seek_forward"); // forward steps add one ordinal + emitter.instruction("cbz x1, __rt_aptr_seek_invalid"); // stepping back off the front lands on the invalid cursor + emitter.instruction("sub x0, x1, #1"); // x0 = previous ordinal + emitter.instruction("ret"); // return the rewound cursor + emitter.label("__rt_aptr_seek_forward"); + emitter.instruction("add x0, x1, #1"); // x0 = next ordinal + emitter.instruction("cmp x0, x11"); // has the cursor stepped past the last ordinal? + emitter.instruction("b.ge __rt_aptr_seek_invalid"); // stepping past the end lands on the invalid cursor + emitter.instruction("ret"); // return the advanced cursor + emitter.label("__rt_aptr_seek_first"); + emitter.instruction("mov x0, #0"); // reset selects the first ordinal + emitter.instruction("ret"); // return the rewound cursor + emitter.label("__rt_aptr_seek_last"); + emitter.instruction("sub x0, x11, #1"); // end selects the final ordinal + emitter.instruction("ret"); // return the advanced cursor + emitter.label("__rt_aptr_seek_invalid"); + emitter.instruction("mov x0, #-1"); // -1 is the canonical invalid cursor + emitter.instruction("ret"); // return the invalid cursor +} + +/// x86_64 Linux implementation of `__rt_array_ptr_seek`. +/// Input: rdi = container pointer, rsi = current cursor, rdx = seek mode +/// Output: rax = new cursor, or `-1` for the invalid position +fn emit_array_ptr_seek_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_ptr_seek ---"); + emitter.label_global("__rt_array_ptr_seek"); + emit_normalize_x86_64(emitter, "__rt_aptr_seek", "__rt_aptr_seek_invalid"); + emitter.instruction("test r11, r11"); // is the container empty? + emitter.instruction("je __rt_aptr_seek_invalid"); // an empty container has no valid position at all + emitter.instruction("cmp rdx, 0"); // is this a reset (mode 0)? + emitter.instruction("je __rt_aptr_seek_first"); // reset rewinds to the first ordinal + emitter.instruction("cmp rdx, 1"); // is this an end (mode 1)? + emitter.instruction("je __rt_aptr_seek_last"); // end jumps to the final ordinal + emitter.instruction("cmp rsi, 0"); // is the incoming cursor before the first ordinal? + emitter.instruction("jl __rt_aptr_seek_invalid"); // an already-invalid cursor stays invalid + emitter.instruction("cmp rsi, r11"); // is the incoming cursor past the last ordinal? + emitter.instruction("jge __rt_aptr_seek_invalid"); // a stale past-the-end cursor stays invalid + emitter.instruction("cmp rdx, 2"); // is this a next (mode 2)? + emitter.instruction("je __rt_aptr_seek_forward"); // forward steps add one ordinal + emitter.instruction("test rsi, rsi"); // is the cursor already on the first ordinal? + emitter.instruction("je __rt_aptr_seek_invalid"); // stepping back off the front lands on the invalid cursor + emitter.instruction("lea rax, [rsi - 1]"); // rax = previous ordinal + emitter.instruction("ret"); // return the rewound cursor + emitter.label("__rt_aptr_seek_forward"); + emitter.instruction("lea rax, [rsi + 1]"); // rax = next ordinal + emitter.instruction("cmp rax, r11"); // has the cursor stepped past the last ordinal? + emitter.instruction("jge __rt_aptr_seek_invalid"); // stepping past the end lands on the invalid cursor + emitter.instruction("ret"); // return the advanced cursor + emitter.label("__rt_aptr_seek_first"); + emitter.instruction("xor eax, eax"); // reset selects the first ordinal + emitter.instruction("ret"); // return the rewound cursor + emitter.label("__rt_aptr_seek_last"); + emitter.instruction("lea rax, [r11 - 1]"); // end selects the final ordinal + emitter.instruction("ret"); // return the advanced cursor + emitter.label("__rt_aptr_seek_invalid"); + emitter.instruction("mov rax, -1"); // -1 is the canonical invalid cursor + emitter.instruction("ret"); // return the invalid cursor +} + +/// array_ptr_key: box the key at a logical cursor as a Mixed cell (`key()`). +/// +/// Out-of-range cursors box canonical null, which is exactly what PHP's `key()` returns +/// once the internal pointer has run off either end. Indexed keys are the ordinal itself +/// because elephc's indexed storage is dense; hash keys come from the ordinal's entry. +/// +/// Input: x0 = container pointer, x1 = cursor ordinal +/// Output: x0 = boxed Mixed key, or boxed null when the cursor is invalid +pub fn emit_array_ptr_key(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_ptr_key_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_ptr_key ---"); + emitter.label_global("__rt_array_ptr_key"); + emit_normalize_aarch64(emitter, "__rt_aptr_key", "__rt_aptr_key_null"); + emitter.instruction("cmp x1, #0"); // is the cursor before the first ordinal? + emitter.instruction("b.lt __rt_aptr_key_null"); // invalid cursors have no key + emitter.instruction("cmp x1, x11"); // is the cursor past the last ordinal? + emitter.instruction("b.ge __rt_aptr_key_null"); // invalid cursors have no key + emitter.instruction("cmp x12, #3"); // is the container an associative hash? + emitter.instruction("b.eq __rt_aptr_key_hash"); // hashes read the key out of the ordinal's entry + emitter.instruction("mov x0, #0"); // dense indexed keys are the ordinal: tag 0 (integer) + emitter.instruction("mov x2, #0"); // value_hi unused for integers + emitter.instruction("b __rt_mixed_from_value"); // box the integer key and return it to the caller + emitter.label("__rt_aptr_key_hash"); + emit_hash_walk_aarch64(emitter, "__rt_aptr_key", "__rt_aptr_key_null"); + emitter.instruction("ldr x9, [x10, #16]"); // x9 = key_len (-1 marks an integer key) + emitter.instruction("ldr x13, [x10, #8]"); // x13 = key payload (integer value or string pointer) + emitter.instruction("cmn x9, #1"); // is the entry keyed by an integer? + emitter.instruction("b.eq __rt_aptr_key_int"); // integer keys box with tag 0 + emitter.instruction("mov x0, #1"); // value_tag = 1 (string) + emitter.instruction("mov x1, x13"); // value_lo = key string pointer + emitter.instruction("mov x2, x9"); // value_hi = key string length + emitter.instruction("b __rt_mixed_from_value"); // box (and persist) the string key and return it + emitter.label("__rt_aptr_key_int"); + emitter.instruction("mov x0, #0"); // value_tag = 0 (integer) + emitter.instruction("mov x1, x13"); // value_lo = integer key + emitter.instruction("mov x2, #0"); // value_hi unused for integers + emitter.instruction("b __rt_mixed_from_value"); // box the integer key and return it to the caller + emitter.label("__rt_aptr_key_null"); + emitter.instruction("mov x0, #8"); // value_tag = 8 (null) + emitter.instruction("mov x1, #0"); // canonical null has no low payload word + emitter.instruction("mov x2, #0"); // value_hi unused + emitter.instruction("b __rt_mixed_from_value"); // box canonical null and return it to the caller +} + +/// x86_64 Linux implementation of `__rt_array_ptr_key`. +/// Input: rdi = container pointer, rsi = cursor ordinal +/// Output: rax = boxed Mixed key, or boxed null when the cursor is invalid +fn emit_array_ptr_key_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_ptr_key ---"); + emitter.label_global("__rt_array_ptr_key"); + emit_normalize_x86_64(emitter, "__rt_aptr_key", "__rt_aptr_key_null"); + emitter.instruction("cmp rsi, 0"); // is the cursor before the first ordinal? + emitter.instruction("jl __rt_aptr_key_null"); // invalid cursors have no key + emitter.instruction("cmp rsi, r11"); // is the cursor past the last ordinal? + emitter.instruction("jge __rt_aptr_key_null"); // invalid cursors have no key + emitter.instruction("movzx eax, BYTE PTR [rdi - 8]"); // reload the low-byte heap kind after normalization + emitter.instruction("cmp eax, 3"); // is the container an associative hash? + emitter.instruction("je __rt_aptr_key_hash"); // hashes read the key out of the ordinal's entry + emitter.instruction("mov rdi, rsi"); // dense indexed keys are the ordinal itself + emitter.instruction("xor esi, esi"); // value_hi unused for integers + emitter.instruction("mov rax, 0"); // value_tag = 0 (integer) + emitter.instruction("jmp __rt_mixed_from_value"); // box the integer key and return it to the caller + emitter.label("__rt_aptr_key_hash"); + emit_hash_walk_x86_64(emitter, "__rt_aptr_key", "__rt_aptr_key_null"); + emitter.instruction("mov r8, QWORD PTR [r10 + 16]"); // r8 = key_len (-1 marks an integer key) + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // r9 = key payload (integer value or string pointer) + emitter.instruction("cmp r8, -1"); // is the entry keyed by an integer? + emitter.instruction("je __rt_aptr_key_int"); // integer keys box with tag 0 + emitter.instruction("mov rdi, r9"); // value_lo = key string pointer + emitter.instruction("mov rsi, r8"); // value_hi = key string length + emitter.instruction("mov rax, 1"); // value_tag = 1 (string) + emitter.instruction("jmp __rt_mixed_from_value"); // box (and persist) the string key and return it + emitter.label("__rt_aptr_key_int"); + emitter.instruction("mov rdi, r9"); // value_lo = integer key + emitter.instruction("xor esi, esi"); // value_hi unused for integers + emitter.instruction("mov rax, 0"); // value_tag = 0 (integer) + emitter.instruction("jmp __rt_mixed_from_value"); // box the integer key and return it to the caller + emitter.label("__rt_aptr_key_null"); + emitter.instruction("xor edi, edi"); // canonical null has no low payload word + emitter.instruction("xor esi, esi"); // value_hi unused + emitter.instruction("mov rax, 8"); // value_tag = 8 (null) + emitter.instruction("jmp __rt_mixed_from_value"); // box canonical null and return it to the caller +} + +/// array_ptr_value: box the value at a logical cursor as a Mixed cell. +/// +/// This backs `current()` and the value half of `next`/`prev`/`reset`/`end`. Out-of-range +/// cursors box `false`, matching PHP's return value once the pointer is off the end — and +/// PHP has the same `false`-vs-`false` ambiguity for an element that really holds `false`. +/// +/// Input: x0 = container pointer, x1 = cursor ordinal +/// Output: x0 = boxed Mixed value, or boxed `false` when the cursor is invalid +pub fn emit_array_ptr_value(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_ptr_value_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_ptr_value ---"); + emitter.label_global("__rt_array_ptr_value"); + emit_normalize_aarch64(emitter, "__rt_aptr_val", "__rt_aptr_val_false"); + emitter.instruction("cmp x1, #0"); // is the cursor before the first ordinal? + emitter.instruction("b.lt __rt_aptr_val_false"); // invalid cursors have no value + emitter.instruction("cmp x1, x11"); // is the cursor past the last ordinal? + emitter.instruction("b.ge __rt_aptr_val_false"); // invalid cursors have no value + emitter.instruction("cmp x12, #3"); // is the container an associative hash? + emitter.instruction("b.eq __rt_aptr_val_hash"); // hashes box the entry payload directly + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer indexed key + emitter.instruction("mov x3, #0"); // never warn: the cursor was already bounds-checked + emitter.instruction("b __rt_array_get_mixed_key"); // reuse the ordinary indexed read path and return its box + emitter.label("__rt_aptr_val_hash"); + emit_hash_walk_aarch64(emitter, "__rt_aptr_val", "__rt_aptr_val_false"); + emitter.instruction("ldr x9, [x10, #24]"); // x9 = value_lo from the hash entry + emitter.instruction("ldr x13, [x10, #32]"); // x13 = value_hi from the hash entry + emitter.instruction("ldr x14, [x10, #40]"); // x14 = value_tag from the hash entry + emitter.instruction("mov x0, x14"); // value_tag = the entry's runtime tag + emitter.instruction("mov x1, x9"); // value_lo = the entry's low payload word + emitter.instruction("mov x2, x13"); // value_hi = the entry's high payload word + emitter.instruction("b __rt_mixed_from_value"); // retain/persist the payload and return the box + emitter.label("__rt_aptr_val_false"); + emitter.instruction("mov x0, #3"); // value_tag = 3 (bool) + emitter.instruction("mov x1, #0"); // value_lo = 0 (false) + emitter.instruction("mov x2, #0"); // value_hi unused + emitter.instruction("b __rt_mixed_from_value"); // box PHP false and return it to the caller +} + +/// x86_64 Linux implementation of `__rt_array_ptr_value`. +/// Input: rdi = container pointer, rsi = cursor ordinal +/// Output: rax = boxed Mixed value, or boxed `false` when the cursor is invalid +fn emit_array_ptr_value_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_ptr_value ---"); + emitter.label_global("__rt_array_ptr_value"); + emit_normalize_x86_64(emitter, "__rt_aptr_val", "__rt_aptr_val_false"); + emitter.instruction("cmp rsi, 0"); // is the cursor before the first ordinal? + emitter.instruction("jl __rt_aptr_val_false"); // invalid cursors have no value + emitter.instruction("cmp rsi, r11"); // is the cursor past the last ordinal? + emitter.instruction("jge __rt_aptr_val_false"); // invalid cursors have no value + emitter.instruction("movzx eax, BYTE PTR [rdi - 8]"); // reload the low-byte heap kind after normalization + emitter.instruction("cmp eax, 3"); // is the container an associative hash? + emitter.instruction("je __rt_aptr_val_hash"); // hashes box the entry payload directly + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer indexed key + emitter.instruction("xor ecx, ecx"); // never warn: the cursor was already bounds-checked + emitter.instruction("jmp __rt_array_get_mixed_key"); // reuse the ordinary indexed read path and return its box + emitter.label("__rt_aptr_val_hash"); + emit_hash_walk_x86_64(emitter, "__rt_aptr_val", "__rt_aptr_val_false"); + emitter.instruction("mov r8, QWORD PTR [r10 + 24]"); // r8 = value_lo from the hash entry + emitter.instruction("mov r9, QWORD PTR [r10 + 32]"); // r9 = value_hi from the hash entry + emitter.instruction("mov rax, QWORD PTR [r10 + 40]"); // rax = value_tag from the hash entry + emitter.instruction("mov rdi, r8"); // value_lo = the entry's low payload word + emitter.instruction("mov rsi, r9"); // value_hi = the entry's high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // retain/persist the payload and return the box + emitter.label("__rt_aptr_val_false"); + emitter.instruction("xor edi, edi"); // value_lo = 0 (false) + emitter.instruction("xor esi, esi"); // value_hi unused + emitter.instruction("mov rax, 3"); // value_tag = 3 (bool) + emitter.instruction("jmp __rt_mixed_from_value"); // box PHP false and return it to the caller +} diff --git a/src/codegen_support/runtime/arrays/array_new.rs b/src/codegen_support/runtime/arrays/array_new.rs index c0203e6950..61e5810fd7 100644 --- a/src/codegen_support/runtime/arrays/array_new.rs +++ b/src/codegen_support/runtime/arrays/array_new.rs @@ -7,9 +7,14 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - `capacity * elem_size` is validated before the allocation request: an unchecked product +//! wraps to a tiny allocation while the header keeps the pre-overflow capacity, so every +//! caller's fill loop would then run outside the block. Overflow is a fatal error. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::data::ARRAY_ALLOC_SIZE_MSG; /// Emits the `__rt_array_new` runtime helper for array allocation. @@ -27,6 +32,11 @@ use crate::codegen_support::platform::Arch; /// `[length:8][capacity:8][elem_size:8][elements...]` /// /// The kind word at `header - 8` encodes the array variant (indexed array, copy-on-write flag, string-array layout hint). +/// +/// # Size validation +/// Negative capacities are clamped to an empty payload region (callers' fill loops use signed +/// comparisons and write nothing), and any `capacity * elem_size + 24` that does not fit in a +/// non-negative machine word terminates the process through `__rt_array_cap_overflow`. pub fn emit_array_new(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_array_new_linux_x86_64(emitter); @@ -37,6 +47,16 @@ pub fn emit_array_new(emitter: &mut Emitter) { emitter.comment("--- runtime: array_new ---"); emitter.label_global("__rt_array_new"); + // -- validate the requested allocation size before touching the heap -- + emitter.instruction("cmp x0, #0"); // is the requested capacity negative? + emitter.instruction("csel x9, x0, xzr, ge"); // clamp negative capacities to an empty payload region + emitter.instruction("umulh x10, x9, x1"); // x10 = high 64 bits of capacity * elem_size + emitter.instruction("cbnz x10, __rt_array_cap_overflow"); // reject payload sizes that do not fit in one machine word + emitter.instruction("mul x9, x9, x1"); // x9 = low 64 bits of capacity * elem_size + emitter.instruction("adds x9, x9, #24"); // x9 = payload size plus the 24-byte array header + emitter.instruction("b.hs __rt_array_cap_overflow"); // reject totals that carried out of the machine word + emitter.instruction("tbnz x9, #63, __rt_array_cap_overflow"); // reject totals the signed heap-size check would read as negative + // -- set up stack frame, save arguments for use after heap_alloc call -- emitter.instruction("sub sp, sp, #48"); // allocate 48 bytes on the stack emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address @@ -45,9 +65,8 @@ pub fn emit_array_new(emitter: &mut Emitter) { emitter.instruction("str x1, [sp, #8]"); // save elem_size to stack (need it after bl) emitter.instruction("str xzr, [sp, #16]"); // keep a reserved scratch slot for future array metadata helpers - // -- calculate total bytes needed: 24-byte header + (capacity * elem_size) -- - emitter.instruction("mul x2, x0, x1"); // x2 = capacity * elem_size = data region size - emitter.instruction("add x0, x2, #24"); // x0 = data size + 24-byte header + // -- allocate the validated total: 24-byte header + (capacity * elem_size) -- + emitter.instruction("mov x0, x9"); // x0 = validated data size + 24-byte header emitter.instruction("bl __rt_heap_alloc"); // allocate memory, x0 = pointer to array emitter.instruction("ldr x9, [sp, #8]"); // reload elem_size for the default packed metadata choice emitter.instruction("cmp x9, #16"); // does the array store 16-byte string payloads? @@ -69,6 +88,15 @@ pub fn emit_array_new(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #48"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = array pointer + + // -- fatal error: requested array size cannot be represented -- + emitter.label("__rt_array_cap_overflow"); + emitter.instruction("mov x0, #2"); // fd = stderr + abi::emit_symbol_address(emitter, "x1", "_arr_cap_err_msg"); + emitter.instruction(&format!("mov x2, #{}", ARRAY_ALLOC_SIZE_MSG.len())); // pass the exact array-size diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 + emitter.syscall(1); } /// Emits the x86_64 Linux implementation of `__rt_array_new`. @@ -88,6 +116,11 @@ pub fn emit_array_new(emitter: &mut Emitter) { /// /// The kind word at `header - 8` includes the x86_64 heap magic marker (`0x454C5048_XXXX_XXXX`), /// the copy-on-write flag (`0x8000`), and the indexed-array kind tag (`2`). +/// +/// # Size validation +/// Mirrors the ARM64 guard: negative capacities are clamped to an empty payload region and any +/// `capacity * elem_size + 24` that does not fit in a non-negative machine word terminates the +/// process through `__rt_array_cap_overflow`. fn emit_array_new_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: array_new ---"); @@ -98,9 +131,15 @@ fn emit_array_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 16"); // reserve local slots for capacity and element size across the heap allocation helper call emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save capacity across the heap allocation helper call emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save element size across the heap allocation helper call - emitter.instruction("imul rdi, rsi"); // compute the data region size as capacity * elem_size before handing it to the heap wrapper - emitter.instruction("add rdi, 24"); // include the fixed 24-byte array header in the owned heap allocation size - emitter.instruction("mov rax, rdi"); // move the total array allocation size into the x86_64 heap helper input register + + // -- validate the requested allocation size before touching the heap -- + emitter.instruction("xor rax, rax"); // default the sizing operand to an empty payload region + emitter.instruction("test rdi, rdi"); // is the requested capacity strictly positive? + emitter.instruction("cmovg rax, rdi"); // clamp negative capacities to an empty payload region + emitter.instruction("imul rax, rsi"); // rax = capacity * elem_size, overflow flag set when the product does not fit + emitter.instruction("jo __rt_array_cap_overflow"); // reject payload sizes that do not fit in one machine word + emitter.instruction("add rax, 24"); // include the fixed 24-byte array header in the owned heap allocation size + emitter.instruction("jo __rt_array_cap_overflow"); // reject totals the signed heap-size accounting would read as negative emitter.instruction("call __rt_heap_alloc"); // allocate the array backing storage through the shared x86_64 heap wrapper emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the element size so the array kind word can encode the string-value layout hint emitter.instruction("cmp r10, 16"); // detect string arrays that use 16-byte payload slots @@ -120,4 +159,15 @@ fn emit_array_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 16"); // release the temporary capacity and element-size spill slots emitter.instruction("pop rbp"); // restore the caller frame pointer before returning emitter.instruction("ret"); // return the array pointer in rax + + // -- fatal error: requested array size cannot be represented -- + emitter.label("__rt_array_cap_overflow"); + emitter.instruction("mov edi, 2"); // fd = stderr for the array-size fatal error message + abi::emit_symbol_address(emitter, "rsi", "_arr_cap_err_msg"); + emitter.instruction(&format!("mov edx, {}", ARRAY_ALLOC_SIZE_MSG.len())); // pass the exact array-size diagnostic byte count + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // print the fatal array-size message to stderr + emitter.instruction("mov edi, 1"); // exit code 1 for an unrepresentable array size + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the array-size failure } diff --git a/src/codegen_support/runtime/arrays/array_pad.rs b/src/codegen_support/runtime/arrays/array_pad.rs index 34c15bfed4..27141597c9 100644 --- a/src/codegen_support/runtime/arrays/array_pad.rs +++ b/src/codegen_support/runtime/arrays/array_pad.rs @@ -7,6 +7,9 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - `abs(size)` is clamped: `INT64_MIN` has no representable magnitude, so the negation is +//! forced to zero instead of wrapping back to a negative destination length. Callers still +//! bound `$length` in lowering, where PHP's `ValueError` is raised. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -15,6 +18,8 @@ use crate::codegen_support::platform::Arch; /// Input: x0 = array pointer, x1 = size (negative = pad left), x2 = pad value /// Output: x0 = pointer to new padded array /// If abs(size) <= current length, returns a copy of the original array. +/// The absolute size is computed once, clamped to a non-negative value, and reused for both +/// the destination capacity and the destination header length so neither can go negative. pub fn emit_array_pad(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_array_pad_linux_x86_64(emitter); @@ -26,9 +31,9 @@ pub fn emit_array_pad(emitter: &mut Emitter) { emitter.label_global("__rt_array_pad"); // -- set up stack frame, save arguments -- - emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #48"); // set up new frame pointer + emitter.instruction("sub sp, sp, #80"); // allocate 80 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // set up new frame pointer emitter.instruction("str x0, [sp, #0]"); // save source array pointer emitter.instruction("str x1, [sp, #8]"); // save size argument emitter.instruction("str x2, [sp, #16]"); // save pad value @@ -39,6 +44,8 @@ pub fn emit_array_pad(emitter: &mut Emitter) { emitter.instruction("cmp x1, #0"); // check if size is negative emitter.instruction("b.ge __rt_array_pad_positive"); // if non-negative, pad right emitter.instruction("neg x3, x1"); // x3 = abs(size) for negative case + emitter.instruction("cmp x3, #0"); // INT64_MIN has no representable magnitude and stays negative here + emitter.instruction("csel x3, x3, xzr, ge"); // clamp that wrapped magnitude to zero so no length is ever negative emitter.instruction("mov x4, #1"); // x4 = 1 (flag: pad left) emitter.instruction("b __rt_array_pad_check"); // continue to size check @@ -48,24 +55,22 @@ pub fn emit_array_pad(emitter: &mut Emitter) { // -- check if padding is needed -- emitter.label("__rt_array_pad_check"); + emitter.instruction("str x3, [sp, #48]"); // keep the clamped abs(size) so no later path recomputes it emitter.instruction("cmp x3, x9"); // compare abs(size) with current length emitter.instruction("b.le __rt_array_pad_copy"); // if abs(size) <= length, just copy - emitter.instruction("str x3, [sp, #32]"); // save abs(size) = new array size emitter.instruction("str x4, [sp, #40]"); // save pad direction flag // -- create new array with capacity = abs(size) -- emitter.instruction("mov x0, x3"); // x0 = capacity = abs(size) emitter.instruction("mov x1, #8"); // x1 = elem_size = 8 (integers) emitter.instruction("bl __rt_array_new"); // allocate new array - emitter.instruction("str x0, [sp, #32]"); // reuse slot to save new array ptr temporarily + emitter.instruction("str x0, [sp, #32]"); // save new array ptr // -- determine pad count and data offset -- emitter.instruction("ldr x9, [sp, #24]"); // x9 = source length emitter.instruction("ldr x4, [sp, #40]"); // x4 = pad direction (0=right, 1=left) - emitter.instruction("ldr x3, [sp, #8]"); // x3 = original size argument - emitter.instruction("cmp x3, #0"); // recheck sign for abs - emitter.instruction("b.ge __rt_array_pad_calc_right"); // positive = pad right - emitter.instruction("neg x3, x3"); // x3 = abs(size) + emitter.instruction("ldr x3, [sp, #48]"); // x3 = clamped abs(size) + emitter.instruction("cbz x4, __rt_array_pad_calc_right"); // pad-right flag = 0 selects the append layout // -- pad left: fill pad values first, then copy source -- emitter.instruction("sub x5, x3, x9"); // x5 = pad_count = abs(size) - length @@ -126,12 +131,10 @@ pub fn emit_array_pad(emitter: &mut Emitter) { // -- set total length and return -- emitter.label("__rt_array_pad_finish"); emitter.instruction("ldr x0, [sp, #32]"); // x0 = new array pointer - emitter.instruction("ldr x3, [sp, #8]"); // x3 = original size argument - emitter.instruction("cmp x3, #0"); // check sign - emitter.instruction("cneg x3, x3, lt"); // x3 = abs(size) + emitter.instruction("ldr x3, [sp, #48]"); // x3 = clamped abs(size), never negative emitter.instruction("str x3, [x0]"); // set array length = abs(size) - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = padded array // -- no padding needed: just create a copy -- @@ -155,8 +158,8 @@ pub fn emit_array_pad(emitter: &mut Emitter) { emitter.label("__rt_array_pad_copy_done"); emitter.instruction("str x9, [x0]"); // set array length = source length - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = copied array } @@ -181,6 +184,9 @@ fn emit_array_pad_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, 0"); // detect whether the caller requested left padding by passing a negative target size emitter.instruction("jge __rt_array_pad_abs_ready_x86"); // skip the absolute-value conversion when the requested target size is already non-negative emitter.instruction("neg r11"); // convert the negative target size into its absolute padded length + emitter.instruction("xor eax, eax"); // stage a zero for the magnitude that INT64_MIN cannot represent + emitter.instruction("test r11, r11"); // detect the wrapped negation that leaves the magnitude negative + emitter.instruction("cmovs r11, rax"); // clamp that wrapped magnitude to zero so no padded length is ever negative emitter.label("__rt_array_pad_abs_ready_x86"); emitter.instruction("cmp r11, r10"); // compare the absolute requested target length against the current source indexed-array length diff --git a/src/codegen_support/runtime/arrays/array_pad_refcounted.rs b/src/codegen_support/runtime/arrays/array_pad_refcounted.rs index 17783bab59..cf53d17f5a 100644 --- a/src/codegen_support/runtime/arrays/array_pad_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_pad_refcounted.rs @@ -7,6 +7,9 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - `abs(size)` is clamped: `INT64_MIN` has no representable magnitude, so the negation is +//! forced to zero instead of wrapping back to a negative pad count. Callers still bound +//! `$length` in lowering, where PHP's `ValueError` is raised. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -14,6 +17,8 @@ use crate::codegen_support::platform::Arch; /// Emits the `__rt_array_pad_refcounted` runtime helper. /// Input: x0 = array pointer, x1 = size (negative = pad left), x2 = borrowed pad payload. /// Output: x0 = pointer to new padded array. +/// The normalized target size is clamped to a non-negative value before the pad count is +/// derived, so a magnitude the machine word cannot hold cannot produce a negative pad count. /// Dispatches to the x86_64 Linux variant; ARM64 uses the native implementation below. pub fn emit_array_pad_refcounted(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -39,6 +44,8 @@ pub fn emit_array_pad_refcounted(emitter: &mut Emitter) { emitter.instruction("cmp x1, #0"); // check whether caller requested left-padding emitter.instruction("b.ge __rt_array_pad_ref_positive"); // skip negation for right-padding emitter.instruction("neg x3, x1"); // compute absolute target size + emitter.instruction("cmp x3, #0"); // INT64_MIN has no representable magnitude and stays negative here + emitter.instruction("csel x3, x3, xzr, ge"); // clamp that wrapped magnitude to zero so the pad count is never negative emitter.instruction("mov x4, #1"); // remember that padding goes on the left emitter.instruction("b __rt_array_pad_ref_check"); // continue with normalized size @@ -141,6 +148,9 @@ fn emit_array_pad_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, 0"); // detect the negative-target-size case that means pad on the left emitter.instruction("jge __rt_array_pad_ref_abs_ready_x86"); // skip negation when the requested target size already pads on the right emitter.instruction("neg r11"); // normalize the requested target size to its absolute magnitude + emitter.instruction("xor eax, eax"); // stage a zero for the magnitude that INT64_MIN cannot represent + emitter.instruction("test r11, r11"); // detect the wrapped negation that leaves the magnitude negative + emitter.instruction("cmovs r11, rax"); // clamp that wrapped magnitude to zero so the pad count is never negative emitter.instruction("mov rcx, 1"); // remember that the requested target size was negative so padding must happen on the left emitter.label("__rt_array_pad_ref_abs_ready_x86"); diff --git a/src/codegen_support/runtime/arrays/array_reduce_str.rs b/src/codegen_support/runtime/arrays/array_reduce_str.rs new file mode 100644 index 0000000000..4e4a25c0d0 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_reduce_str.rs @@ -0,0 +1,133 @@ +//! Purpose: +//! Emits the `__rt_array_reduce_str` runtime helper assembly used by +//! `array_reduce()` when the source is an indexed string array. String arrays +//! store 16-byte `[ptr:8][len:8]` payload slots, so the 8-byte element loader in +//! `__rt_array_reduce` would misread them. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - The accumulator stays a single integer-register value; only the element is +//! widened to a pointer/length pair. The lowering side rejects string +//! accumulators, so no intermediate string ever has to be persisted or freed +//! here. +//! - Callback ABI: AArch64 `x0` = accumulator, `x1`/`x2` = element pointer/length, +//! `x3` = optional capture environment; x86_64 `rdi`, `rsi`/`rdx`, `rcx`. The +//! new accumulator is read back from `x0`/`rax`. +//! - All loop state lives in the frame because the callback may clobber every +//! caller-saved register; the helper touches no callee-saved register other +//! than the frame pointer. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// array_reduce_str: folds an indexed string array into one integer accumulator. +/// +/// Input: AArch64 `x0` = callback address, `x1` = source array pointer, +/// `x2` = initial accumulator, `x3` = optional capture environment pointer; +/// x86_64 `rdi` / `rsi` / `rdx` / `rcx` respectively. +/// Output: `x0` / `rax` = the accumulator returned by the final callback call, or +/// the initial value when the source array is empty. +pub fn emit_array_reduce_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_reduce_str_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_reduce_str ---"); + emitter.label_global("__rt_array_reduce_str"); + + // Frame (80 bytes): [0]=data base [8]=length [16]=i [24]=accumulator + // [32]=callback [40]=env [64]=x29,x30 + emitter.instruction("sub sp, sp, #80"); // reserve the fold state that must survive callback calls + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #32]"); // save the callback address for every loop iteration + emitter.instruction("str x2, [sp, #24]"); // seed the accumulator with the initial value + emitter.instruction("str x3, [sp, #40]"); // save the optional callback capture environment pointer + emitter.instruction("ldr x9, [x1]"); // x9 = source array length from the header + emitter.instruction("str x9, [sp, #8]"); // save the source array length + emitter.instruction("add x9, x1, #24"); // x9 = base of the data region (skip header) + emitter.instruction("str x9, [sp, #0]"); // save the data base + emitter.instruction("mov x9, xzr"); // loop index i = 0 + emitter.instruction("str x9, [sp, #16]"); // save i + + emitter.label("__rt_array_reduce_str_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload i + emitter.instruction("ldr x10, [sp, #8]"); // reload the source array length + emitter.instruction("cmp x9, x10"); // compare i with the source array length + emitter.instruction("b.ge __rt_array_reduce_str_done"); // i >= length: the fold is complete + emitter.instruction("ldr x10, [sp, #0]"); // reload the data base + emitter.instruction("add x10, x10, x9, lsl #4"); // x10 = &data[i] (16-byte string slots) + emitter.instruction("ldr x0, [sp, #24]"); // callback arg 0: current accumulator + emitter.instruction("ldr x1, [x10]"); // callback arg 1: element string pointer + emitter.instruction("ldr x2, [x10, #8]"); // callback arg 2: element string length + emitter.instruction("ldr x3, [sp, #40]"); // pass the capture environment after the element pair + emitter.instruction("ldr x9, [sp, #32]"); // reload the callback address + emitter.instruction("blr x9"); // x0 = callback(accumulator, element) + emitter.instruction("str x0, [sp, #24]"); // accumulator = callback result + emitter.instruction("ldr x9, [sp, #16]"); // reload i after the callback clobbered caller-saved registers + emitter.instruction("add x9, x9, #1"); // i += 1 + emitter.instruction("str x9, [sp, #16]"); // save i + emitter.instruction("b __rt_array_reduce_str_loop"); // continue folding the remaining elements + + emitter.label("__rt_array_reduce_str_done"); + emitter.instruction("ldr x0, [sp, #24]"); // return the final accumulator + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the fold state frame + emitter.instruction("ret"); // return with x0 = accumulated value +} + +/// x86_64 Linux implementation of the `__rt_array_reduce_str` runtime helper. +/// +/// Inputs (System V): `rdi` = callback address, `rsi` = source array pointer, +/// `rdx` = initial accumulator, `rcx` = optional capture environment pointer. +/// The callback is invoked with `rdi` = accumulator, `rsi`/`rdx` = element +/// pointer/length, `rcx` = environment, and returns the new accumulator in `rax`. +/// Emits `__rt_array_reduce_str` as a global label. +fn emit_array_reduce_str_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_reduce_str ---"); + emitter.label_global("__rt_array_reduce_str"); + + // Frame (rbp-relative): [-8]=data base [-16]=length [-24]=i + // [-32]=accumulator [-40]=callback [-48]=env + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve the fold state slots and keep rsp 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the callback address for every loop iteration + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // seed the accumulator with the initial value + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the optional callback capture environment pointer + emitter.instruction("mov r8, QWORD PTR [rsi]"); // r8 = source array length from the header + emitter.instruction("mov QWORD PTR [rbp - 16], r8"); // save the source array length + emitter.instruction("lea r8, [rsi + 24]"); // r8 = base of the data region (skip header) + emitter.instruction("mov QWORD PTR [rbp - 8], r8"); // save the data base + emitter.instruction("xor r8d, r8d"); // loop index i = 0 + emitter.instruction("mov QWORD PTR [rbp - 24], r8"); // save i + + emitter.label("__rt_array_reduce_str_loop_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload i + emitter.instruction("cmp r9, QWORD PTR [rbp - 16]"); // compare i with the source array length + emitter.instruction("jge __rt_array_reduce_str_done_linux_x86_64"); // i >= length: the fold is complete + emitter.instruction("shl r9, 4"); // i * 16 (16-byte string slots) + emitter.instruction("add r9, QWORD PTR [rbp - 8]"); // r9 = &data[i] + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // callback arg 0: current accumulator + emitter.instruction("mov rsi, QWORD PTR [r9]"); // callback arg 1: element string pointer + emitter.instruction("mov rdx, QWORD PTR [r9 + 8]"); // callback arg 2: element string length + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // pass the capture environment after the element pair + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the callback address + emitter.instruction("call r11"); // rax = callback(accumulator, element) + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // accumulator = callback result + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload i after the callback clobbered caller-saved registers + emitter.instruction("add r9, 1"); // i += 1 + emitter.instruction("mov QWORD PTR [rbp - 24], r9"); // save i + emitter.instruction("jmp __rt_array_reduce_str_loop_linux_x86_64"); // continue folding the remaining elements + + emitter.label("__rt_array_reduce_str_done_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the final accumulator + emitter.instruction("add rsp, 48"); // release the fold state slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with rax = accumulated value +} diff --git a/src/codegen_support/runtime/arrays/array_set_mixed_key.rs b/src/codegen_support/runtime/arrays/array_set_mixed_key.rs index 7e860e8f8e..358fee6e33 100644 --- a/src/codegen_support/runtime/arrays/array_set_mixed_key.rs +++ b/src/codegen_support/runtime/arrays/array_set_mixed_key.rs @@ -81,7 +81,7 @@ pub fn emit_array_set_mixed_key(emitter: &mut Emitter) { emitter.instruction("cmp x0, #2"); // float mixed keys are cast to integer keys like PHP emitter.instruction("b.ne __rt_array_set_mixed_key_int_ready"); // integer/bool keys are already valid indexed indexes emitter.instruction("fmov d0, x1"); // load the float key payload into the FP register - emitter.instruction("fcvtzs x1, d0"); // cast the float key to an integer index like PHP + abi::emit_php_float_to_int(emitter, "x1"); // cast the float key to an integer index with PHP float->int rules emitter.label("__rt_array_set_mixed_key_int_ready"); emitter.instruction("ldr x0, [sp, #0]"); // reload the indexed-array pointer emitter.instruction("cmp x1, #0"); // negative int keys cannot live in packed indexed storage @@ -167,7 +167,7 @@ pub fn emit_array_set_mixed_key(emitter: &mut Emitter) { emitter.instruction("cmp x0, #2"); // float mixed keys are cast to integer keys like PHP emitter.instruction("b.ne __rt_array_set_mixed_key_hash_int"); // integer/bool keys become scalar integer hash keys emitter.instruction("fmov d0, x1"); // load the float key payload into the FP register - emitter.instruction("fcvtzs x1, d0"); // cast the float key to an integer hash key like PHP + abi::emit_php_float_to_int(emitter, "x1"); // cast the float key to an integer hash key with PHP float->int rules emitter.label("__rt_array_set_mixed_key_hash_int"); emitter.instruction("mov x2, #-1"); // key_hi sentinel marks scalar integer hash keys emitter.instruction("b __rt_array_set_mixed_key_hash_set"); // proceed to the hash insert with an integer key @@ -230,7 +230,7 @@ fn emit_array_set_mixed_key_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, 2"); // float mixed keys are cast to integer keys like PHP emitter.instruction("jne __rt_array_set_mixed_key_int_ready"); // integer/bool keys are already valid indexed indexes emitter.instruction("movq xmm0, rdi"); // load the float key payload into the FP register - emitter.instruction("cvttsd2si rdi, xmm0"); // cast the float key to an integer index like PHP + abi::emit_php_float_to_int(emitter, "rdi"); // cast the float key to an integer index with PHP float->int rules emitter.label("__rt_array_set_mixed_key_int_ready"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the indexed-array pointer emitter.instruction("cmp rdi, 0"); // negative int keys cannot live in packed indexed storage @@ -315,7 +315,7 @@ fn emit_array_set_mixed_key_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, 2"); // float mixed keys are cast to integer keys like PHP emitter.instruction("jne __rt_array_set_mixed_key_hash_int"); // integer/bool keys become scalar integer hash keys emitter.instruction("movq xmm0, rdi"); // load the float key payload into the FP register - emitter.instruction("cvttsd2si rdi, xmm0"); // cast the float key to an integer hash key like PHP + abi::emit_php_float_to_int(emitter, "rdi"); // cast the float key to an integer hash key with PHP float->int rules emitter.label("__rt_array_set_mixed_key_hash_int"); emitter.instruction("mov rsi, rdi"); // publish the integer key payload as the hash key low word emitter.instruction("mov rdx, -1"); // key_hi sentinel marks scalar integer hash keys diff --git a/src/codegen_support/runtime/arrays/array_slice.rs b/src/codegen_support/runtime/arrays/array_slice.rs index e714de7705..26a81fc8d0 100644 --- a/src/codegen_support/runtime/arrays/array_slice.rs +++ b/src/codegen_support/runtime/arrays/array_slice.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_array_slice`, `__rt_array_slice_pos_off` runtime helper assembly for array slice. +//! Emits the `__rt_array_slice` runtime helper assembly for array slice. //! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,22 +7,25 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - The slice window is normalized by the shared `slice_bounds` prologue, so the copy loop always +//! runs over a non-negative element count that lies inside the source payload. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; /// Emits the `__rt_array_slice` runtime helper for ARM64. /// Extracts a contiguous slice from an integer array, returning a new array. /// /// # ABI (ARM64) -/// - Input: x0 = source array pointer, x1 = byte offset, x2 = slice length -/// - x2 = -1 indicates "read to end of array" +/// - Input: x0 = source array pointer, x1 = `$offset`, x2 = `$length`, +/// x3 = 1 when `$length` was supplied, 0 when it was omitted or `null` /// - Output: x0 = pointer to newly allocated sliced array /// /// # Behavior -/// - Negative offset counts from end (e.g., -2 means length-2) -/// - Offset is clamped to [0, array_len]; if offset >= length, returns empty array -/// - Length is clamped to [0, remaining_elements_from_offset] +/// - Offset/length normalization is delegated to `emit_slice_bounds`, which applies PHP's rules: +/// negative offsets count from the end, an omitted length runs to the end, and a negative length +/// stops that many elements before the end (clamped to an empty result). /// - Calls `__rt_array_new` to allocate the result array pub fn emit_array_slice(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -39,25 +42,9 @@ pub fn emit_array_slice(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address emitter.instruction("add x29, sp, #48"); // set up new frame pointer emitter.instruction("str x0, [sp, #0]"); // save source array pointer - emitter.instruction("ldr x9, [x0]"); // x9 = source array length - emitter.instruction("str x9, [sp, #8]"); // save source length - // -- handle negative offset: convert to positive -- - emitter.instruction("cmp x1, #0"); // check if offset is negative - emitter.instruction("b.ge __rt_array_slice_pos_off"); // if non-negative, skip adjustment - emitter.instruction("add x1, x9, x1"); // offset = length + offset (e.g., -2 → length-2) - emitter.instruction("cmp x1, #0"); // clamp to 0 if still negative - emitter.instruction("csel x1, xzr, x1, lt"); // if offset < 0, set to 0 - - // -- compute actual slice length -- - emitter.label("__rt_array_slice_pos_off"); - emitter.instruction("cmp x1, x9"); // check if offset >= array length - emitter.instruction("b.ge __rt_array_slice_empty"); // if so, result is empty array - emitter.instruction("sub x3, x9, x1"); // x3 = max possible length = array_len - offset - emitter.instruction("cmn x2, #1"); // check if length == -1 (to end) - emitter.instruction("csel x2, x3, x2, eq"); // if length == -1, use remaining length - emitter.instruction("cmp x2, x3"); // clamp length to max possible - emitter.instruction("csel x2, x3, x2, gt"); // if length > remaining, use remaining + // -- normalize the requested slice window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_slice"); emitter.instruction("str x1, [sp, #16]"); // save computed offset emitter.instruction("str x2, [sp, #24]"); // save computed slice length @@ -92,18 +79,13 @@ pub fn emit_array_slice(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = sliced array - - // -- empty result: offset was beyond array bounds -- - emitter.label("__rt_array_slice_empty"); - emitter.instruction("mov x0, #0"); // x0 = capacity = 0 - emitter.instruction("mov x1, #8"); // x1 = elem_size = 8 - emitter.instruction("bl __rt_array_new"); // allocate empty array - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame - emitter.instruction("ret"); // return with x0 = empty array } /// Emits the `__rt_array_slice` runtime helper for x86_64 Linux. +/// +/// Same slice semantics as the ARM64 variant; only the System V register encoding differs: +/// `rdi` = source array pointer, `rsi` = `$offset`, `rdx` = `$length`, `rcx` = 1 when a `$length` +/// was supplied and 0 when it was omitted or `null`; the sliced array is returned in `rax`. fn emit_array_slice_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: array_slice ---"); @@ -113,29 +95,9 @@ fn emit_array_slice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source indexed-array pointer, computed offset, slice length, and result pointer emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the scalar slice bookkeeping while keeping nested constructor calls 16-byte aligned emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source indexed-array pointer across slice-length normalization and result-array construction - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before normalizing the slice offset and requested length - emitter.instruction("cmp rsi, 0"); // detect the negative-offset case that counts backward from the end of the source indexed array - emitter.instruction("jge __rt_array_slice_pos_off_x86"); // skip the backward-from-end adjustment when the requested slice offset is already non-negative - emitter.instruction("add rsi, r10"); // convert a negative slice offset into a source-length-relative positive offset - emitter.instruction("cmp rsi, 0"); // clamp the normalized slice offset so it never points before the start of the source indexed array - emitter.instruction("jge __rt_array_slice_pos_off_x86"); // keep the normalized slice offset when it no longer points before the start of the source indexed array - emitter.instruction("xor esi, esi"); // clamp the normalized slice offset to zero when it would still point before the source array start - - emitter.label("__rt_array_slice_pos_off_x86"); - emitter.instruction("cmp rsi, r10"); // detect the out-of-bounds offset case before allocating the destination indexed array - emitter.instruction("jge __rt_array_slice_empty_x86"); // return an empty indexed array when the requested slice offset starts beyond the source length - emitter.instruction("mov rcx, r10"); // seed the maximum removable-length scratch register from the source indexed-array logical length - emitter.instruction("sub rcx, rsi"); // compute the remaining scalar payload count from the normalized slice offset to the end of the source indexed array - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_slice should run until the end of the source indexed array - emitter.instruction("jne __rt_array_slice_known_len_x86"); // keep the explicit requested slice length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining scalar payload count in the source indexed array - emitter.label("__rt_array_slice_known_len_x86"); - emitter.instruction("cmp rdx, rcx"); // clamp the requested slice length so it cannot extend beyond the source indexed-array bounds - emitter.instruction("jle __rt_array_slice_len_ready_x86"); // keep the explicit requested slice length when it already fits inside the remaining scalar payload window - emitter.instruction("mov rdx, rcx"); // clamp the requested slice length down to the remaining scalar payload count - - emitter.label("__rt_array_slice_len_ready_x86"); + // -- normalize the requested slice window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_slice"); emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the normalized slice offset across the destination indexed-array constructor call emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the clamped slice length across the destination indexed-array constructor call emitter.instruction("mov rdi, rdx"); // pass the clamped slice length as the destination indexed-array capacity to the shared constructor @@ -166,12 +128,4 @@ fn emit_array_slice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 32"); // release the scalar slice spill slots before returning to the caller emitter.instruction("pop rbp"); // restore the caller frame pointer after the scalar slice helper completes emitter.instruction("ret"); // return the destination indexed-array pointer in rax - - emitter.label("__rt_array_slice_empty_x86"); - emitter.instruction("mov rdi, 0"); // request an empty destination indexed-array capacity when the normalized slice offset starts beyond the source length - emitter.instruction("mov rsi, 8"); // request 8-byte scalar payload slots for the empty destination indexed array - emitter.instruction("call __rt_array_new"); // allocate the empty destination indexed array through the shared x86_64 constructor - emitter.instruction("add rsp, 32"); // release the scalar slice spill slots before returning the empty destination indexed array - emitter.instruction("pop rbp"); // restore the caller frame pointer after the empty-slice constructor path - emitter.instruction("ret"); // return the empty destination indexed-array pointer in rax } diff --git a/src/codegen_support/runtime/arrays/array_slice_refcounted.rs b/src/codegen_support/runtime/arrays/array_slice_refcounted.rs index 54930b7d17..070c67d3e7 100644 --- a/src/codegen_support/runtime/arrays/array_slice_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_slice_refcounted.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_array_slice_refcounted`, `__rt_array_slice_ref_pos_off` runtime helper assembly for array slice refcounted. +//! Emits the `__rt_array_slice_refcounted` runtime helper assembly for array slice refcounted. //! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,9 +7,12 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - The slice window is normalized by the shared `slice_bounds` prologue, so the retain loop always +//! runs over a non-negative element count that lies inside the source payload. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; /// Emits the `__rt_array_slice_refcounted` runtime helper. /// @@ -17,16 +20,16 @@ use crate::codegen_support::platform::Arch; /// is never modified; the returned array owns its elements independently. /// /// ## ABI -/// - **ARM64**: `x0` = source array pointer, `x1` = start offset, `x2` = length (`-1` = till end). -/// Result returned in `x0`. -/// - **x86_64 Linux**: `rdi` = source array pointer, `rsi` = start offset, `rdx` = length. +/// - **ARM64**: `x0` = source array pointer, `x1` = `$offset`, `x2` = `$length`, `x3` = 1 when a +/// `$length` was supplied and 0 when it was omitted or `null`. Result returned in `x0`. +/// - **x86_64 Linux**: `rdi`, `rsi`, `rdx`, `rcx` with the same meaning. /// Result returned in `rax`. /// /// ## Slice semantics -/// - Negative offset: counted backward from end of source, clamped to 0. -/// - Length `-1` (ARM64) / `-1` sentinel: slice from offset to end of source. -/// - Length exceeding remaining elements: clamped to available elements. -/// - Offset out of range: returns an empty array. +/// Delegated to `emit_slice_bounds`: negative offsets count backward from the end and clamp to the +/// start, an omitted `$length` slices to the end, a negative `$length` stops that many elements +/// before the end (clamped to an empty result), and a positive `$length` is clamped to the elements +/// actually available. An offset past the end yields an empty array. pub fn emit_array_slice_refcounted(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_array_slice_refcounted_linux_x86_64(emitter); @@ -42,24 +45,9 @@ pub fn emit_array_slice_refcounted(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address emitter.instruction("add x29, sp, #48"); // set up new frame pointer emitter.instruction("str x0, [sp, #0]"); // save source array pointer - emitter.instruction("ldr x9, [x0]"); // load source array length - emitter.instruction("str x9, [sp, #8]"); // save source length - // -- handle negative offset: convert to positive -- - emitter.instruction("cmp x1, #0"); // test whether offset is negative - emitter.instruction("b.ge __rt_array_slice_ref_pos_off"); // skip adjustment for non-negative offsets - emitter.instruction("add x1, x9, x1"); // convert negative offset into positive index - emitter.instruction("cmp x1, #0"); // clamp converted offset against zero - emitter.instruction("csel x1, xzr, x1, lt"); // use zero when converted offset is still negative - - emitter.label("__rt_array_slice_ref_pos_off"); - emitter.instruction("cmp x1, x9"); // compare offset with source length - emitter.instruction("b.ge __rt_array_slice_ref_empty"); // return empty array when offset is out of range - emitter.instruction("sub x3, x9, x1"); // compute maximum possible slice length - emitter.instruction("cmn x2, #1"); // check whether requested length is -1 - emitter.instruction("csel x2, x3, x2, eq"); // use remaining length when caller requested -1 - emitter.instruction("cmp x2, x3"); // compare requested length with remaining length - emitter.instruction("csel x2, x3, x2, gt"); // clamp requested length to remaining length + // -- normalize the requested slice window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_slice_ref"); emitter.instruction("str x1, [sp, #16]"); // save normalized offset emitter.instruction("str x2, [sp, #24]"); // save normalized slice length @@ -91,14 +79,6 @@ pub fn emit_array_slice_refcounted(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return sliced array - - emitter.label("__rt_array_slice_ref_empty"); - emitter.instruction("mov x0, #0"); // request zero-capacity destination array - emitter.instruction("mov x1, #8"); // use 8-byte slots for heap pointers - emitter.instruction("bl __rt_array_new"); // allocate empty destination array - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame - emitter.instruction("ret"); // return empty sliced array } /// Emits the x86_64 Linux variant of `__rt_array_slice_refcounted`. @@ -114,29 +94,9 @@ fn emit_array_slice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source indexed-array pointer, normalized offset, clamped length, and destination array emitter.instruction("sub rsp, 48"); // reserve aligned spill slots for the refcounted slice bookkeeping while keeping helper calls 16-byte aligned emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source indexed-array pointer across slice normalization and destination construction - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before normalizing the requested slice offset and length - emitter.instruction("cmp rsi, 0"); // detect the negative-offset case that counts backward from the end of the source indexed array - emitter.instruction("jge __rt_array_slice_ref_pos_off_x86"); // skip the backward-from-end adjustment when the requested slice offset is already non-negative - emitter.instruction("add rsi, r10"); // convert a negative slice offset into a source-length-relative positive offset - emitter.instruction("cmp rsi, 0"); // clamp the normalized slice offset so it never points before the start of the source indexed array - emitter.instruction("jge __rt_array_slice_ref_pos_off_x86"); // keep the normalized slice offset when it no longer points before the start of the source indexed array - emitter.instruction("xor esi, esi"); // clamp the normalized slice offset to zero when it would still point before the source array start - - emitter.label("__rt_array_slice_ref_pos_off_x86"); - emitter.instruction("cmp rsi, r10"); // detect the out-of-bounds offset case before allocating the destination indexed array - emitter.instruction("jge __rt_array_slice_ref_empty_x86"); // return an empty indexed array when the requested slice offset starts beyond the source length - emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length - emitter.instruction("sub rcx, rsi"); // compute the remaining refcounted payload count from the normalized slice offset to the end of the source indexed array - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_slice should run until the end of the source indexed array - emitter.instruction("jne __rt_array_slice_ref_known_len_x86"); // keep the explicit requested slice length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining refcounted payload count in the source indexed array - emitter.label("__rt_array_slice_ref_known_len_x86"); - emitter.instruction("cmp rdx, rcx"); // clamp the requested slice length so it cannot extend beyond the source indexed-array bounds - emitter.instruction("jle __rt_array_slice_ref_len_ready_x86"); // keep the explicit requested slice length when it already fits inside the remaining refcounted payload window - emitter.instruction("mov rdx, rcx"); // clamp the requested slice length down to the remaining refcounted payload count - - emitter.label("__rt_array_slice_ref_len_ready_x86"); + // -- normalize the requested slice window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_slice_ref"); emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the normalized slice offset across the destination indexed-array constructor call emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the clamped slice length across the destination indexed-array constructor call emitter.instruction("mov rdi, rdx"); // pass the clamped slice length as the destination indexed-array capacity to the shared constructor @@ -171,12 +131,4 @@ fn emit_array_slice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 48"); // release the refcounted slice spill slots before returning emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the refcounted sliced indexed-array pointer in rax - - emitter.label("__rt_array_slice_ref_empty_x86"); - emitter.instruction("mov rdi, 0"); // request an empty destination indexed-array capacity when the normalized slice offset starts beyond the source length - emitter.instruction("mov rsi, 8"); // request 8-byte payload slots for the empty destination indexed array - emitter.instruction("call __rt_array_new"); // allocate the empty destination indexed array through the shared x86_64 constructor - emitter.instruction("add rsp, 48"); // release the refcounted slice spill slots before returning the empty destination indexed array - emitter.instruction("pop rbp"); // restore the caller frame pointer after the empty-slice constructor path - emitter.instruction("ret"); // return the empty refcounted sliced indexed-array pointer in rax } diff --git a/src/codegen_support/runtime/arrays/array_slice_to_hash.rs b/src/codegen_support/runtime/arrays/array_slice_to_hash.rs new file mode 100644 index 0000000000..8cad66d039 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_slice_to_hash.rs @@ -0,0 +1,190 @@ +//! Purpose: +//! Emits the `__rt_array_slice_to_hash` runtime helper backing `array_slice($a, $o, $l, true)`. +//! Copies the PHP slice window out of an indexed array into an owned hash that keeps each +//! element's ORIGINAL integer key instead of renumbering it from zero. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - The `$offset`/`$length` window is normalized by the shared `emit_slice_bounds` prologue, the +//! single source of truth for PHP's slice arithmetic, so the key-preserving form cannot drift +//! from `__rt_array_slice` / `__rt_array_slice_refcounted` — negative offsets, negative lengths +//! and out-of-range clamps behave identically and the copied window always stays inside the +//! source payload. +//! - The element extraction mirrors `__rt_array_to_hash` slot for slot: string elements +//! (16-byte slots) are persisted into independent heap copies, heap-backed elements are +//! retained, and scalar elements are copied by value, so the result owns its payloads. +//! - PHP's `preserve_keys` result is key-identical to the sliced source region, which is exactly +//! what a hash records and elephc's dense indexed array cannot represent. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; + +/// array_slice_to_hash: build an owned hash {start: e(start), …} from an indexed array window. +/// Input: x0 = indexed array pointer, x1 = raw `$offset`, x2 = raw `$length`, +/// x3 = 1 when the caller passed a `$length` and 0 when it was omitted or `null` +/// Output: x0 = new owned hash carrying the source integer keys of the selected window +/// +/// Backs `array_slice($array, $offset, $length, preserve_keys: true)`. +pub fn emit_array_slice_to_hash(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_slice_to_hash_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_slice_to_hash ---"); + emitter.label_global("__rt_array_slice_to_hash"); + emitter.instruction("sub sp, sp, #80"); // allocate the conversion stack frame + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // set up the new frame pointer + emit_slice_bounds(emitter, "__rt_array_slice_to_hash"); + emitter.instruction("str x0, [sp, #0]"); // save the indexed array pointer + emitter.instruction("str x1, [sp, #16]"); // cursor i = normalized window start + emitter.instruction("add x9, x1, x2"); // end = start + clamped window length + emitter.instruction("str x9, [sp, #24]"); // save the exclusive window end + emitter.instruction("ldr x10, [x0, #-8]"); // load the uniform heap-kind header word + emitter.instruction("lsr x10, x10, #8"); // shift the packed value_type into the low bits + emitter.instruction("and x10, x10, #0x7f"); // isolate the indexed-array value_type (also the Mixed tag) + emitter.instruction("str x10, [sp, #32]"); // save the value_type / runtime tag + emitter.instruction("ldr x11, [x0, #16]"); // load the element size (stride) from the header + emitter.instruction("str x11, [sp, #40]"); // save the element stride + emitter.instruction("mov x1, x10"); // value_type for the new hash header + emitter.instruction("cmp x2, #8"); // is the window below the minimum hash capacity? + emitter.instruction("b.ge __rt_array_slice_to_hash_cap_ok"); // use the window length as the capacity hint + emitter.instruction("mov x2, #8"); // clamp the capacity hint to a small minimum + emitter.label("__rt_array_slice_to_hash_cap_ok"); + emitter.instruction("mov x0, x2"); // capacity hint for the new hash + emitter.instruction("bl __rt_hash_new"); // allocate the result hash, x0 = result + emitter.instruction("str x0, [sp, #8]"); // save the result hash pointer + emitter.label("__rt_array_slice_to_hash_loop"); + emitter.instruction("ldr x10, [sp, #16]"); // reload the window cursor + emitter.instruction("ldr x9, [sp, #24]"); // reload the exclusive window end + emitter.instruction("cmp x10, x9"); // has the cursor reached the end of the window? + emitter.instruction("b.ge __rt_array_slice_to_hash_done"); // the whole window has been copied + emitter.instruction("ldr x11, [sp, #0]"); // reload the indexed array pointer + emitter.instruction("add x11, x11, #24"); // skip the 24-byte indexed-array header + emitter.instruction("ldr x12, [sp, #40]"); // reload the element stride + emitter.instruction("mul x13, x10, x12"); // byte offset of element[i] + emitter.instruction("add x11, x11, x13"); // x11 = address of element[i] + emitter.instruction("ldr x3, [x11]"); // load the element low word + emitter.instruction("str x3, [sp, #48]"); // save the element low word + emitter.instruction("ldr x9, [sp, #32]"); // reload the value_type + emitter.instruction("cmp x9, #1"); // is the element a string? + emitter.instruction("b.eq __rt_array_slice_to_hash_string"); // strings need persistence + emitter.instruction("mov x9, #0"); // non-string elements have no high word + emitter.instruction("str x9, [sp, #56]"); // save a zero high word + emitter.instruction("ldr x9, [sp, #32]"); // reload the value_type + emitter.instruction("cmp x9, #4"); // is the element below the heap-backed tag range? + emitter.instruction("b.lt __rt_array_slice_to_hash_set"); // scalar elements need no retain + emitter.instruction("cmp x9, #7"); // is the element above the heap-backed tag range? + emitter.instruction("b.gt __rt_array_slice_to_hash_set"); // non-heap tags need no retain + emitter.instruction("ldr x0, [sp, #48]"); // load the heap-backed element pointer + emitter.instruction("bl __rt_incref"); // retain the heap-backed element for the result hash + emitter.instruction("b __rt_array_slice_to_hash_set"); // continue to insertion + emitter.label("__rt_array_slice_to_hash_string"); + emitter.instruction("ldr x2, [x11, #8]"); // load the string length from the 16-byte slot + emitter.instruction("ldr x1, [sp, #48]"); // load the string pointer + emitter.instruction("bl __rt_str_persist"); // copy the string into an independent heap block, x1 = new pointer + emitter.instruction("str x1, [sp, #48]"); // save the persisted string pointer + emitter.instruction("str x2, [sp, #56]"); // save the string length + emitter.label("__rt_array_slice_to_hash_set"); + emitter.instruction("ldr x0, [sp, #8]"); // result hash pointer + emitter.instruction("ldr x1, [sp, #16]"); // integer key = the preserved source index i + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer key + emitter.instruction("ldr x3, [sp, #48]"); // value low word + emitter.instruction("ldr x4, [sp, #56]"); // value high word + emitter.instruction("ldr x5, [sp, #32]"); // value runtime tag (= value_type) + emitter.instruction("bl __rt_hash_set"); // insert element[i] at its preserved integer key + emitter.instruction("str x0, [sp, #8]"); // update the result pointer after possible reallocation + emitter.instruction("ldr x10, [sp, #16]"); // reload the window cursor + emitter.instruction("add x10, x10, #1"); // advance to the next element of the window + emitter.instruction("str x10, [sp, #16]"); // save the advanced cursor + emitter.instruction("b __rt_array_slice_to_hash_loop"); // continue copying the window + emitter.label("__rt_array_slice_to_hash_done"); + emitter.instruction("ldr x0, [sp, #8]"); // x0 = result hash pointer + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate the stack frame + emitter.instruction("ret"); // return the result hash in x0 +} + +/// x86_64 Linux implementation of `__rt_array_slice_to_hash`. +/// Input: rdi = indexed array pointer, rsi = raw `$offset`, rdx = raw `$length`, +/// rcx = 1 when the caller passed a `$length` and 0 when it was omitted or `null` +/// Output: rax = new owned hash carrying the source integer keys of the selected window +fn emit_array_slice_to_hash_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_slice_to_hash ---"); + emitter.label_global("__rt_array_slice_to_hash"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("sub rsp, 80"); // reserve local slots for the conversion loop state + emit_slice_bounds(emitter, "__rt_array_slice_to_hash"); + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the indexed array pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rsi"); // cursor i = normalized window start + emitter.instruction("mov rax, rsi"); // seed the window-end scratch from the window start + emitter.instruction("add rax, rdx"); // end = start + clamped window length + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the exclusive window end + emitter.instruction("mov r10, QWORD PTR [rdi - 8]"); // load the uniform heap-kind header word + emitter.instruction("shr r10, 8"); // shift the packed value_type into the low bits + emitter.instruction("and r10, 127"); // isolate the indexed-array value_type (also the Mixed tag) + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the value_type / runtime tag + emitter.instruction("mov r11, QWORD PTR [rdi + 16]"); // load the element size (stride) from the header + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the element stride + emitter.instruction("mov rsi, r10"); // value_type for the new hash header + emitter.instruction("mov rdi, rdx"); // capacity hint = clamped window length + emitter.instruction("cmp rdi, 8"); // is the window below the minimum hash capacity? + emitter.instruction("jge __rt_array_slice_to_hash_cap_ok"); // use the window length as the capacity hint + emitter.instruction("mov rdi, 8"); // clamp the capacity hint to a small minimum + emitter.label("__rt_array_slice_to_hash_cap_ok"); + emitter.instruction("call __rt_hash_new"); // allocate the result hash, rax = result + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the result hash pointer + emitter.label("__rt_array_slice_to_hash_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the window cursor + emitter.instruction("cmp rax, QWORD PTR [rbp - 72]"); // has the cursor reached the end of the window? + emitter.instruction("jge __rt_array_slice_to_hash_done"); // the whole window has been copied + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the indexed array pointer + emitter.instruction("add r10, 24"); // skip the 24-byte indexed-array header + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the element stride + emitter.instruction("imul r11, rax"); // byte offset of element[i] + emitter.instruction("add r10, r11"); // r10 = address of element[i] + emitter.instruction("mov rcx, QWORD PTR [r10]"); // load the element low word + emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the element low word + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the value_type + emitter.instruction("cmp r9, 1"); // is the element a string? + emitter.instruction("je __rt_array_slice_to_hash_string"); // strings need persistence + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // non-string elements have no high word + emitter.instruction("cmp r9, 4"); // is the element below the heap-backed tag range? + emitter.instruction("jl __rt_array_slice_to_hash_set"); // scalar elements need no retain + emitter.instruction("cmp r9, 7"); // is the element above the heap-backed tag range? + emitter.instruction("jg __rt_array_slice_to_hash_set"); // non-heap tags need no retain + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // load the heap-backed element pointer + emitter.instruction("call __rt_incref"); // retain the heap-backed element for the result hash + emitter.instruction("jmp __rt_array_slice_to_hash_set"); // continue to insertion + emitter.label("__rt_array_slice_to_hash_string"); + emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the string length from the 16-byte slot + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // load the string pointer + emitter.instruction("call __rt_str_persist"); // copy the string into an independent heap block, rax = new pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the persisted string pointer + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the string length + emitter.label("__rt_array_slice_to_hash_set"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // result hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // integer key = the preserved source index i + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer key + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // value low word + emitter.instruction("mov r8, QWORD PTR [rbp - 64]"); // value high word + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // value runtime tag (= value_type) + emitter.instruction("call __rt_hash_set"); // insert element[i] at its preserved integer key + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // update the result pointer after possible reallocation + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the window cursor + emitter.instruction("add rax, 1"); // advance to the next element of the window + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the advanced cursor + emitter.instruction("jmp __rt_array_slice_to_hash_loop"); // continue copying the window + emitter.label("__rt_array_slice_to_hash_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // rax = result hash pointer + emitter.instruction("add rsp, 80"); // release the local slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the result hash in rax +} diff --git a/src/codegen_support/runtime/arrays/array_splice.rs b/src/codegen_support/runtime/arrays/array_splice.rs index 6aa48d9ee8..482322fc09 100644 --- a/src/codegen_support/runtime/arrays/array_splice.rs +++ b/src/codegen_support/runtime/arrays/array_splice.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_array_splice`, `__rt_array_new` runtime helper assembly for array splice. +//! Emits the `__rt_array_splice` runtime helper assembly for array splice. //! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,9 +7,12 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - The removal window is normalized by the shared `slice_bounds` prologue, so the removal count is +//! always non-negative and the compaction loop never reads or writes outside the source payload. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; /// Emits the `__rt_array_splice` runtime helper for ARM64 and x86_64. /// @@ -17,12 +20,14 @@ use crate::codegen_support::platform::Arch; /// `length` elements and returns those removed elements in a new array. /// /// ## ARM64 ABI -/// - **Input**: `x0` = source array pointer, `x1` = offset (element index), `x2` = removal length -/// - **Output**: `x0` = new array containing the removed elements +/// - **Input**: `x0` = source array pointer, `x1` = `$offset`, `x2` = `$length`, `x3` = 1 when a +/// `$length` was supplied and 0 when it was omitted or `null` +/// - **Output**: `x0` = new array containing the removed elements, `x1` = the normalized removal +/// offset, i.e. the index a `$replacement` is inserted at /// - **Behavior**: The original array is modified in-place; remaining elements shift left to fill the gap. -/// A negative `length` (via `cmp x2, #-1` in caller) acts as an "until-end" sentinel that removes -/// all elements from `offset` to the array end. -/// - **Clamping**: The removal length is clamped to `max(0, array_length - offset)` to prevent out-of-bounds reads. +/// - **Clamping**: `emit_slice_bounds` normalizes the window first, so the removal count is always in +/// `[0, array_length - offset]` — a negative `$length` stops that many elements before the end and +/// an over-large negative one removes nothing. /// - **COW contract**: The result array is freshly allocated; callers receive owned storage. /// - **Stack frame**: 48 bytes allocated; saves x29, x30 and four stack slots for array ptr / offset / length / result ptr. pub fn emit_array_splice(emitter: &mut Emitter) { @@ -47,14 +52,10 @@ pub fn emit_array_splice(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("add x29, sp, #32"); // set up new frame pointer emitter.instruction("str x0, [sp, #0]"); // save source array pointer - emitter.instruction("str x1, [sp, #8]"); // save offset - emitter.instruction("str x2, [sp, #16]"); // save removal length - - // -- clamp removal length to not exceed array bounds -- - emitter.instruction("ldr x3, [x0]"); // x3 = source array length - emitter.instruction("sub x4, x3, x1"); // x4 = length - offset (max removable) - emitter.instruction("cmp x2, x4"); // compare requested length with max - emitter.instruction("csel x2, x4, x2, gt"); // clamp to max if too large + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice"); + emitter.instruction("str x1, [sp, #8]"); // save normalized offset emitter.instruction("str x2, [sp, #16]"); // save clamped removal length // -- create result array for removed elements -- @@ -78,6 +79,7 @@ pub fn emit_array_splice(emitter: &mut Emitter) { emitter.instruction("ldr x1, [x5, x9, lsl #3]"); // x1 = source[offset + j] emitter.instruction("ldr x0, [sp, #24]"); // x0 = result array pointer emitter.instruction("bl __rt_array_push_int"); // push to result array + emitter.instruction("str x0, [sp, #24]"); // persist the result pointer in case the append reallocated it emitter.instruction("add x8, x8, #1"); // j += 1 emitter.instruction("b __rt_array_splice_copy"); // continue copying @@ -111,6 +113,7 @@ pub fn emit_array_splice(emitter: &mut Emitter) { // -- return result array -- emitter.instruction("ldr x0, [sp, #24]"); // x0 = result array pointer + emitter.instruction("ldr x1, [sp, #8]"); // x1 = normalized offset, the index a $replacement is inserted at emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #48"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = removed elements array @@ -121,8 +124,10 @@ pub fn emit_array_splice(emitter: &mut Emitter) { /// Identical in behavior to the ARM64 variant but uses x86_64 calling conventions and register set. /// /// ## x86_64 ABI -/// - **Input**: `rdi` = source array pointer, `rsi` = offset, `rdx` = removal length (or `-1` sentinel for until-end) -/// - **Output**: `rax` = new array containing the removed elements +/// - **Input**: `rdi` = source array pointer, `rsi` = `$offset`, `rdx` = `$length`, `rcx` = 1 when a +/// `$length` was supplied and 0 when it was omitted or `null` +/// - **Output**: `rax` = new array containing the removed elements, `rdx` = the normalized removal +/// offset, i.e. the index a `$replacement` is inserted at /// - **Behavior**: Same semantics as ARM64 — in-place mutation, left-shift to fill gap, clamped length. /// - **Frame layout**: 32-byte aligned spill area at `[rbp - 8]` through `[rbp - 32]` preserves: /// source array pointer, offset, clamped length, and result array pointer across constructor calls. @@ -136,20 +141,10 @@ fn emit_array_splice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source indexed-array pointer, normalized removal length, and result pointer emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the scalar splice bookkeeping while keeping nested constructor calls 16-byte aligned emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source indexed-array pointer across removal-length clamping and result-array construction - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the requested splice offset across the result-array constructor call - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before clamping the requested removal length - emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length - emitter.instruction("sub rcx, rsi"); // compute the maximum removable scalar payload count from the requested splice offset - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_splice should remove until the end of the source indexed array - emitter.instruction("jne __rt_array_splice_known_len_x86"); // keep the explicit requested removal length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining scalar payload count in the source indexed array - - emitter.label("__rt_array_splice_known_len_x86"); - emitter.instruction("cmp rdx, rcx"); // clamp the requested removal length so it never extends beyond the source indexed-array bounds - emitter.instruction("jle __rt_array_splice_len_ready_x86"); // keep the explicit requested removal length when it already fits inside the remaining scalar payload window - emitter.instruction("mov rdx, rcx"); // clamp the requested removal length down to the maximum removable scalar payload count - - emitter.label("__rt_array_splice_len_ready_x86"); + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice"); + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the normalized splice offset across the result-array constructor call emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the clamped removal length across the result-array constructor call emitter.instruction("mov rdi, rdx"); // pass the clamped removal length as the result indexed-array capacity to the shared constructor emitter.instruction("mov rsi, 8"); // request 8-byte scalar payload slots for the result indexed array @@ -197,6 +192,7 @@ fn emit_array_splice_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [r10], r11"); // persist the shortened source indexed-array logical length back into the array header emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the result indexed-array pointer before publishing its logical length emitter.instruction("mov QWORD PTR [rax], r9"); // store the clamped removal length as the result indexed-array logical length + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // return the normalized removal offset, the index a $replacement is inserted at emitter.instruction("add rsp, 32"); // release the scalar splice spill slots before returning to the caller emitter.instruction("pop rbp"); // restore the caller frame pointer after the scalar splice helper completes emitter.instruction("ret"); // return the result indexed-array pointer in rax diff --git a/src/codegen_support/runtime/arrays/array_splice_insert.rs b/src/codegen_support/runtime/arrays/array_splice_insert.rs new file mode 100644 index 0000000000..68d9be448a --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_splice_insert.rs @@ -0,0 +1,945 @@ +//! Purpose: +//! Emits `__rt_array_splice_insert`, `__rt_array_splice_insert_refcounted`, +//! `__rt_array_splice_insert_boxed`, and `__rt_array_splice_insert_unboxed`, the runtime helpers +//! that write `array_splice()`'s `$replacement` values into the gap the removal opened. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! - The EIR lowering of `array_splice()` in `crate::codegen::lower_inst::builtins::arrays`. +//! +//! Key details: +//! - The destination is grown through `__rt_array_grow` BEFORE anything is written, so the +//! right-slide and the replacement copy always stay inside the payload allocation. Growth may +//! relocate the array, which is why the caller stores the returned pointer back into the +//! by-reference receiver. +//! - The tail slide walks backwards, so a replacement longer than the removed window cannot +//! overwrite an element it has not moved yet. +//! - The four variants differ only in what one replacement slot becomes in the destination: +//! copied verbatim (scalar), retained first (refcounted, so the replacement array keeps its own +//! reference), wrapped in a freshly allocated Mixed cell (boxed, for a heterogeneous +//! destination fed a typed replacement), or read out of a Mixed cell as a plain integer +//! (unboxed, for a typed destination fed a boxed replacement such as `[$x + 1]`). +//! - The insertion index is clamped to `[0, length]` in every variant, so a caller that hands +//! over an unnormalized offset still cannot write outside the payload. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits `__rt_array_splice_insert` for the active target. +/// +/// ## ARM64 ABI +/// - **Input**: `x0` = destination indexed array, `x1` = insertion index, `x2` = replacement +/// indexed array (0 inserts nothing) +/// - **Output**: `x0` = the possibly-relocated destination indexed array +/// +/// ## x86_64 ABI +/// - **Input**: `rdi` = destination, `rsi` = insertion index, `rdx` = replacement +/// - **Output**: `rax` = the possibly-relocated destination indexed array +/// +/// Payload slots are copied verbatim, which is correct for the 8-byte scalar element types +/// (`int`, `bool`, `float`, `callable`) this variant serves. +pub fn emit_array_splice_insert(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_insert_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert ---"); + emitter.label_global("__rt_array_splice_insert"); + + // Stack layout: [sp,#0] destination array, [sp,#8] replacement array, + // [sp,#16] insertion index, [sp,#24] copy loop index, + // [sp,#32] payload scratch, [sp,#40] source value_type tag, + // [sp,#48] saved x29/x30. + emitter.instruction("sub sp, sp, #64"); // reserve the insertion bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination indexed-array pointer across growth + emitter.instruction("str x2, [sp, #8]"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("str x1, [sp, #16]"); // preserve the requested insertion index across growth + emitter.instruction("str xzr, [sp, #24]"); // start the replacement copy loop at the first replacement slot + emitter.instruction("cbz x2, __rt_asi_done"); // a null replacement inserts nothing + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("cbz x9, __rt_asi_done"); // an empty replacement inserts nothing + + // -- propagate the replacement's value_type so an empty destination is tagged -- + emitter.instruction("ldr x10, [x0, #-8]"); // load the destination packed array kind word + emitter.instruction("ldr x11, [x2, #-8]"); // load the replacement packed array kind word + emitter.instruction("and x11, x11, #0x7f00"); // keep only the replacement value_type lane + emitter.instruction("mov x12, #0x80ff"); // preserve the destination kind byte and persistent COW flag + emitter.instruction("and x10, x10, x12"); // drop stale destination value_type bits before propagation + emitter.instruction("orr x10, x10, x11"); // combine the destination kind bits with the replacement tag + emitter.instruction("str x10, [x0, #-8]"); // persist the propagated packed array value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("ldr x10, [x0]"); // load the destination logical length + emitter.instruction("ldr x11, [x0, #8]"); // load the destination slot capacity + emitter.instruction("add x12, x10, x9"); // compute the element count the insertion needs room for + emitter.label("__rt_asi_grow_check"); + emitter.instruction("cmp x12, x11"); // does the destination already have room for the insertion? + emitter.instruction("b.le __rt_asi_shift"); // yes, start sliding the tail right + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination pointer before reallocating it + emitter.instruction("bl __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("str x0, [sp, #0]"); // persist the possibly-relocated destination pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement pointer after the growth helper + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count after the growth helper + emitter.instruction("ldr x10, [x0]"); // reload the destination logical length after the growth helper + emitter.instruction("ldr x11, [x0, #8]"); // reload the destination capacity after the growth helper + emitter.instruction("add x12, x10, x9"); // recompute the element count the insertion needs room for + emitter.instruction("b __rt_asi_grow_check"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asi_shift"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("ldr x10, [x0]"); // x10 = destination logical length + emitter.instruction("ldr x1, [sp, #16]"); // x1 = requested insertion index + emitter.instruction("cmp x1, #0"); // did the caller ask to insert before the first slot? + emitter.instruction("csel x1, x1, xzr, ge"); // clamp a negative insertion index to the front + emitter.instruction("cmp x1, x10"); // does the insertion index lie past the last slot? + emitter.instruction("csel x1, x1, x10, lt"); // clamp an over-large insertion index to an append + emitter.instruction("str x1, [sp, #16]"); // persist the clamped insertion index for the copy loop + emitter.instruction("add x3, x0, #24"); // x3 = destination payload base address + emitter.instruction("sub x4, x10, #1"); // x4 = index of the last live destination element + emitter.label("__rt_asi_shift_loop"); + emitter.instruction("cmp x4, x1"); // have all elements at or after the insertion index moved? + emitter.instruction("b.lt __rt_asi_copy"); // yes, write the replacement into the opened gap + emitter.instruction("ldr x5, [x3, x4, lsl #3]"); // load the element that has to slide right + emitter.instruction("add x6, x4, x9"); // compute its slot after the opened gap + emitter.instruction("str x5, [x3, x6, lsl #3]"); // store the element past the opened gap + emitter.instruction("sub x4, x4, #1"); // walk backwards so overlapping slots stay intact + emitter.instruction("b __rt_asi_shift_loop"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asi_copy"); + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("cmp x4, x9"); // has every replacement element been copied? + emitter.instruction("b.ge __rt_asi_set_len"); // yes, publish the extended destination length + emitter.instruction("add x5, x2, #24"); // compute the replacement payload base address + emitter.instruction("ldr x6, [x5, x4, lsl #3]"); // load the borrowed replacement payload + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the clamped insertion index + emitter.instruction("add x3, x0, #24"); // compute the destination payload base address + emitter.instruction("add x7, x1, x4"); // compute the destination slot for this replacement element + emitter.instruction("str x6, [x3, x7, lsl #3]"); // store the replacement payload into the opened gap + emitter.instruction("add x4, x4, #1"); // advance to the next replacement element + emitter.instruction("str x4, [sp, #24]"); // persist the updated replacement copy index + emitter.instruction("b __rt_asi_copy"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asi_set_len"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the previous destination logical length + emitter.instruction("add x10, x10, x9"); // extend it by the inserted element count + emitter.instruction("str x10, [x0]"); // persist the extended destination logical length + + emitter.label("__rt_asi_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the possibly-relocated destination pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the insertion bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits `__rt_array_splice_insert_refcounted` for the active target. +/// +/// Same ABI as [`emit_array_splice_insert`], plus one `__rt_incref` per inserted payload so the +/// destination array and the replacement array each own their own reference. +pub fn emit_array_splice_insert_refcounted(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_insert_refcounted_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_refcounted ---"); + emitter.label_global("__rt_array_splice_insert_refcounted"); + + // Stack layout: [sp,#0] destination array, [sp,#8] replacement array, + // [sp,#16] insertion index, [sp,#24] copy loop index, + // [sp,#32] payload scratch, [sp,#40] source value_type tag, + // [sp,#48] saved x29/x30. + emitter.instruction("sub sp, sp, #64"); // reserve the insertion bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination indexed-array pointer across growth + emitter.instruction("str x2, [sp, #8]"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("str x1, [sp, #16]"); // preserve the requested insertion index across growth + emitter.instruction("str xzr, [sp, #24]"); // start the replacement copy loop at the first replacement slot + emitter.instruction("cbz x2, __rt_asir_done"); // a null replacement inserts nothing + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("cbz x9, __rt_asir_done"); // an empty replacement inserts nothing + + // -- propagate the replacement's value_type so an empty destination is tagged -- + emitter.instruction("ldr x10, [x0, #-8]"); // load the destination packed array kind word + emitter.instruction("ldr x11, [x2, #-8]"); // load the replacement packed array kind word + emitter.instruction("and x11, x11, #0x7f00"); // keep only the replacement value_type lane + emitter.instruction("mov x12, #0x80ff"); // preserve the destination kind byte and persistent COW flag + emitter.instruction("and x10, x10, x12"); // drop stale destination value_type bits before propagation + emitter.instruction("orr x10, x10, x11"); // combine the destination kind bits with the replacement tag + emitter.instruction("str x10, [x0, #-8]"); // persist the propagated packed array value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("ldr x10, [x0]"); // load the destination logical length + emitter.instruction("ldr x11, [x0, #8]"); // load the destination slot capacity + emitter.instruction("add x12, x10, x9"); // compute the element count the insertion needs room for + emitter.label("__rt_asir_grow_check"); + emitter.instruction("cmp x12, x11"); // does the destination already have room for the insertion? + emitter.instruction("b.le __rt_asir_shift"); // yes, start sliding the tail right + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination pointer before reallocating it + emitter.instruction("bl __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("str x0, [sp, #0]"); // persist the possibly-relocated destination pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement pointer after the growth helper + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count after the growth helper + emitter.instruction("ldr x10, [x0]"); // reload the destination logical length after the growth helper + emitter.instruction("ldr x11, [x0, #8]"); // reload the destination capacity after the growth helper + emitter.instruction("add x12, x10, x9"); // recompute the element count the insertion needs room for + emitter.instruction("b __rt_asir_grow_check"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asir_shift"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("ldr x10, [x0]"); // x10 = destination logical length + emitter.instruction("ldr x1, [sp, #16]"); // x1 = requested insertion index + emitter.instruction("cmp x1, #0"); // did the caller ask to insert before the first slot? + emitter.instruction("csel x1, x1, xzr, ge"); // clamp a negative insertion index to the front + emitter.instruction("cmp x1, x10"); // does the insertion index lie past the last slot? + emitter.instruction("csel x1, x1, x10, lt"); // clamp an over-large insertion index to an append + emitter.instruction("str x1, [sp, #16]"); // persist the clamped insertion index for the copy loop + emitter.instruction("add x3, x0, #24"); // x3 = destination payload base address + emitter.instruction("sub x4, x10, #1"); // x4 = index of the last live destination element + emitter.label("__rt_asir_shift_loop"); + emitter.instruction("cmp x4, x1"); // have all elements at or after the insertion index moved? + emitter.instruction("b.lt __rt_asir_copy"); // yes, write the replacement into the opened gap + emitter.instruction("ldr x5, [x3, x4, lsl #3]"); // load the element that has to slide right + emitter.instruction("add x6, x4, x9"); // compute its slot after the opened gap + emitter.instruction("str x5, [x3, x6, lsl #3]"); // store the element past the opened gap + emitter.instruction("sub x4, x4, #1"); // walk backwards so overlapping slots stay intact + emitter.instruction("b __rt_asir_shift_loop"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asir_copy"); + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("cmp x4, x9"); // has every replacement element been copied? + emitter.instruction("b.ge __rt_asir_set_len"); // yes, publish the extended destination length + emitter.instruction("add x5, x2, #24"); // compute the replacement payload base address + emitter.instruction("ldr x6, [x5, x4, lsl #3]"); // load the borrowed replacement payload + emitter.instruction("str x6, [sp, #32]"); // preserve the borrowed payload across the retain call + emitter.instruction("mov x0, x6"); // move the borrowed payload into the retain argument register + emitter.instruction("bl __rt_incref"); // retain it before the destination array becomes an owner + emitter.instruction("ldr x6, [sp, #32]"); // reload the retained payload after the retain call + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index after the retain call + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the clamped insertion index + emitter.instruction("add x3, x0, #24"); // compute the destination payload base address + emitter.instruction("add x7, x1, x4"); // compute the destination slot for this replacement element + emitter.instruction("str x6, [x3, x7, lsl #3]"); // store the replacement payload into the opened gap + emitter.instruction("add x4, x4, #1"); // advance to the next replacement element + emitter.instruction("str x4, [sp, #24]"); // persist the updated replacement copy index + emitter.instruction("b __rt_asir_copy"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asir_set_len"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the previous destination logical length + emitter.instruction("add x10, x10, x9"); // extend it by the inserted element count + emitter.instruction("str x10, [x0]"); // persist the extended destination logical length + + emitter.label("__rt_asir_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the possibly-relocated destination pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the insertion bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits `__rt_array_splice_insert_boxed` for the active target. +/// +/// Takes one extra argument — the replacement's runtime value_type tag in `x3` / `rcx` — and +/// wraps every replacement payload in a fresh `__rt_mixed_from_value` cell, which the destination +/// then owns outright. That is what a heterogeneous `array` receiver needs when the +/// replacement is a typed array such as `[7, 8, 9]` or `["a", "b"]`: the replacement keeps its raw +/// slots and is released by the caller's ordinary temporary cleanup. A string replacement +/// (`tag == 1`) reads 16-byte pointer/length slots and hands the borrowed pair to +/// `__rt_mixed_from_value`, which persists the bytes itself, so the Mixed cell never aliases +/// storage the replacement array still owns. +pub fn emit_array_splice_insert_boxed(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_insert_boxed_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_boxed ---"); + emitter.label_global("__rt_array_splice_insert_boxed"); + + // Stack layout: [sp,#0] destination array, [sp,#8] replacement array, + // [sp,#16] insertion index, [sp,#24] copy loop index, + // [sp,#32] payload scratch, [sp,#40] source value_type tag, + // [sp,#48] saved x29/x30. + emitter.instruction("sub sp, sp, #64"); // reserve the insertion bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination indexed-array pointer across growth + emitter.instruction("str x2, [sp, #8]"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("str x1, [sp, #16]"); // preserve the requested insertion index across growth + emitter.instruction("str xzr, [sp, #24]"); // start the replacement copy loop at the first replacement slot + emitter.instruction("str x3, [sp, #40]"); // preserve the replacement slot value_type tag for the boxing calls + emitter.instruction("cbz x2, __rt_asib_done"); // a null replacement inserts nothing + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("cbz x9, __rt_asib_done"); // an empty replacement inserts nothing + + // -- the destination stores boxed Mixed cells whatever the replacement slots hold -- + emitter.instruction("ldr x10, [x0, #-8]"); // load the destination packed array kind word + emitter.instruction("mov x12, #0x80ff"); // preserve the destination kind byte and persistent COW flag + emitter.instruction("and x10, x10, x12"); // drop stale destination value_type bits before restamping + emitter.instruction("mov x11, #0x700"); // runtime value_type tag 7 marks boxed Mixed payload slots + emitter.instruction("orr x10, x10, x11"); // combine the destination kind bits with the Mixed value_type tag + emitter.instruction("str x10, [x0, #-8]"); // persist the boxed Mixed value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("ldr x10, [x0]"); // load the destination logical length + emitter.instruction("ldr x11, [x0, #8]"); // load the destination slot capacity + emitter.instruction("add x12, x10, x9"); // compute the element count the insertion needs room for + emitter.label("__rt_asib_grow_check"); + emitter.instruction("cmp x12, x11"); // does the destination already have room for the insertion? + emitter.instruction("b.le __rt_asib_shift"); // yes, start sliding the tail right + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination pointer before reallocating it + emitter.instruction("bl __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("str x0, [sp, #0]"); // persist the possibly-relocated destination pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement pointer after the growth helper + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count after the growth helper + emitter.instruction("ldr x10, [x0]"); // reload the destination logical length after the growth helper + emitter.instruction("ldr x11, [x0, #8]"); // reload the destination capacity after the growth helper + emitter.instruction("add x12, x10, x9"); // recompute the element count the insertion needs room for + emitter.instruction("b __rt_asib_grow_check"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asib_shift"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("ldr x10, [x0]"); // x10 = destination logical length + emitter.instruction("ldr x1, [sp, #16]"); // x1 = requested insertion index + emitter.instruction("cmp x1, #0"); // did the caller ask to insert before the first slot? + emitter.instruction("csel x1, x1, xzr, ge"); // clamp a negative insertion index to the front + emitter.instruction("cmp x1, x10"); // does the insertion index lie past the last slot? + emitter.instruction("csel x1, x1, x10, lt"); // clamp an over-large insertion index to an append + emitter.instruction("str x1, [sp, #16]"); // persist the clamped insertion index for the copy loop + emitter.instruction("add x3, x0, #24"); // x3 = destination payload base address + emitter.instruction("sub x4, x10, #1"); // x4 = index of the last live destination element + emitter.label("__rt_asib_shift_loop"); + emitter.instruction("cmp x4, x1"); // have all elements at or after the insertion index moved? + emitter.instruction("b.lt __rt_asib_copy"); // yes, write the replacement into the opened gap + emitter.instruction("ldr x5, [x3, x4, lsl #3]"); // load the element that has to slide right + emitter.instruction("add x6, x4, x9"); // compute its slot after the opened gap + emitter.instruction("str x5, [x3, x6, lsl #3]"); // store the element past the opened gap + emitter.instruction("sub x4, x4, #1"); // walk backwards so overlapping slots stay intact + emitter.instruction("b __rt_asib_shift_loop"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asib_copy"); + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("cmp x4, x9"); // has every replacement element been copied? + emitter.instruction("b.ge __rt_asib_set_len"); // yes, publish the extended destination length + emitter.instruction("add x5, x2, #24"); // compute the replacement payload base address + emitter.instruction("ldr x8, [sp, #40]"); // reload the replacement slot value_type tag + emitter.instruction("cmp x8, #1"); // do the replacement slots hold string pointer/length pairs? + emitter.instruction("b.eq __rt_asib_copy_string"); // string slots need a wider load + emitter.instruction("ldr x1, [x5, x4, lsl #3]"); // load the raw replacement payload from its 8-byte slot + emitter.instruction("mov x2, xzr"); // scalar Mixed payloads leave the high payload word clear + emitter.instruction("b __rt_asib_copy_box"); // the payload words are ready for the Mixed cell allocator + emitter.label("__rt_asib_copy_string"); + emitter.instruction("add x5, x5, x4, lsl #4"); // advance to this element's 16-byte string slot + emitter.instruction("ldr x1, [x5]"); // load the borrowed string pointer from the replacement slot + emitter.instruction("ldr x2, [x5, #8]"); // load the borrowed string length from the replacement slot + emitter.label("__rt_asib_copy_box"); + emitter.instruction("ldr x0, [sp, #40]"); // pass the replacement slot value_type tag to the boxer + emitter.instruction("bl __rt_mixed_from_value"); // allocate one owned Mixed cell for this element + emitter.instruction("mov x6, x0"); // the fresh Mixed cell is what the destination slot receives + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index after the boxing call + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the clamped insertion index + emitter.instruction("add x3, x0, #24"); // compute the destination payload base address + emitter.instruction("add x7, x1, x4"); // compute the destination slot for this replacement element + emitter.instruction("str x6, [x3, x7, lsl #3]"); // store the replacement payload into the opened gap + emitter.instruction("add x4, x4, #1"); // advance to the next replacement element + emitter.instruction("str x4, [sp, #24]"); // persist the updated replacement copy index + emitter.instruction("b __rt_asib_copy"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asib_set_len"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the previous destination logical length + emitter.instruction("add x10, x10, x9"); // extend it by the inserted element count + emitter.instruction("str x10, [x0]"); // persist the extended destination logical length + + emitter.label("__rt_asib_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the possibly-relocated destination pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the insertion bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits `__rt_array_splice_insert_unboxed` for the active target. +/// +/// The mirror of the boxed variant: the replacement holds boxed Mixed cells and the destination +/// stores plain integer slots, so every payload is read back through `__rt_mixed_cast_int`. That +/// is the shape an overflow-checked expression produces — `[$x + 1, $x + 2]` is an `array` +/// because `ichecked_add` boxes its result — spliced into an `array` receiver. Nothing is +/// retained or released: the destination stores a copy of the integer value, and the replacement +/// array keeps owning its cells. +/// +/// `__rt_mixed_cast_int` reads its argument from `x0` / `rax` — it forwards straight into +/// `__rt_mixed_unbox`, whose x86_64 input register is `rax`, not the first SysV argument register. +pub fn emit_array_splice_insert_unboxed(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_insert_unboxed_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_unboxed ---"); + emitter.label_global("__rt_array_splice_insert_unboxed"); + + // Stack layout: [sp,#0] destination array, [sp,#8] replacement array, + // [sp,#16] insertion index, [sp,#24] copy loop index, + // [sp,#32] payload scratch, [sp,#40] source value_type tag, + // [sp,#48] saved x29/x30. + emitter.instruction("sub sp, sp, #64"); // reserve the insertion bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination indexed-array pointer across growth + emitter.instruction("str x2, [sp, #8]"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("str x1, [sp, #16]"); // preserve the requested insertion index across growth + emitter.instruction("str xzr, [sp, #24]"); // start the replacement copy loop at the first replacement slot + emitter.instruction("cbz x2, __rt_asiu_done"); // a null replacement inserts nothing + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("cbz x9, __rt_asiu_done"); // an empty replacement inserts nothing + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("ldr x10, [x0]"); // load the destination logical length + emitter.instruction("ldr x11, [x0, #8]"); // load the destination slot capacity + emitter.instruction("add x12, x10, x9"); // compute the element count the insertion needs room for + emitter.label("__rt_asiu_grow_check"); + emitter.instruction("cmp x12, x11"); // does the destination already have room for the insertion? + emitter.instruction("b.le __rt_asiu_shift"); // yes, start sliding the tail right + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination pointer before reallocating it + emitter.instruction("bl __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("str x0, [sp, #0]"); // persist the possibly-relocated destination pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement pointer after the growth helper + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count after the growth helper + emitter.instruction("ldr x10, [x0]"); // reload the destination logical length after the growth helper + emitter.instruction("ldr x11, [x0, #8]"); // reload the destination capacity after the growth helper + emitter.instruction("add x12, x10, x9"); // recompute the element count the insertion needs room for + emitter.instruction("b __rt_asiu_grow_check"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asiu_shift"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("ldr x10, [x0]"); // x10 = destination logical length + emitter.instruction("ldr x1, [sp, #16]"); // x1 = requested insertion index + emitter.instruction("cmp x1, #0"); // did the caller ask to insert before the first slot? + emitter.instruction("csel x1, x1, xzr, ge"); // clamp a negative insertion index to the front + emitter.instruction("cmp x1, x10"); // does the insertion index lie past the last slot? + emitter.instruction("csel x1, x1, x10, lt"); // clamp an over-large insertion index to an append + emitter.instruction("str x1, [sp, #16]"); // persist the clamped insertion index for the copy loop + emitter.instruction("add x3, x0, #24"); // x3 = destination payload base address + emitter.instruction("sub x4, x10, #1"); // x4 = index of the last live destination element + emitter.label("__rt_asiu_shift_loop"); + emitter.instruction("cmp x4, x1"); // have all elements at or after the insertion index moved? + emitter.instruction("b.lt __rt_asiu_copy"); // yes, write the replacement into the opened gap + emitter.instruction("ldr x5, [x3, x4, lsl #3]"); // load the element that has to slide right + emitter.instruction("add x6, x4, x9"); // compute its slot after the opened gap + emitter.instruction("str x5, [x3, x6, lsl #3]"); // store the element past the opened gap + emitter.instruction("sub x4, x4, #1"); // walk backwards so overlapping slots stay intact + emitter.instruction("b __rt_asiu_shift_loop"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asiu_copy"); + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("cmp x4, x9"); // has every replacement element been copied? + emitter.instruction("b.ge __rt_asiu_set_len"); // yes, publish the extended destination length + emitter.instruction("add x5, x2, #24"); // compute the replacement payload base address + emitter.instruction("ldr x6, [x5, x4, lsl #3]"); // load the borrowed replacement payload + emitter.instruction("mov x0, x6"); // move the boxed Mixed cell into the unbox argument register + emitter.instruction("bl __rt_mixed_cast_int"); // read the cell's integer payload for the typed slot + emitter.instruction("mov x6, x0"); // the plain integer is what the destination slot receives + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index after the unbox call + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the clamped insertion index + emitter.instruction("add x3, x0, #24"); // compute the destination payload base address + emitter.instruction("add x7, x1, x4"); // compute the destination slot for this replacement element + emitter.instruction("str x6, [x3, x7, lsl #3]"); // store the replacement payload into the opened gap + emitter.instruction("add x4, x4, #1"); // advance to the next replacement element + emitter.instruction("str x4, [sp, #24]"); // persist the updated replacement copy index + emitter.instruction("b __rt_asiu_copy"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asiu_set_len"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the previous destination logical length + emitter.instruction("add x10, x10, x9"); // extend it by the inserted element count + emitter.instruction("str x10, [x0]"); // persist the extended destination logical length + + emitter.label("__rt_asiu_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the possibly-relocated destination pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the insertion bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_insert`. +fn emit_array_splice_insert_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert ---"); + emitter.label_global("__rt_array_splice_insert"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before the insertion spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the insertion bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for destination, replacement, and indexes + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve the requested insertion index across growth + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the replacement copy loop at the first replacement slot + emitter.instruction("test rdx, rdx"); // did the caller pass a replacement indexed array at all? + emitter.instruction("jz __rt_asi_done_x86"); // a null replacement inserts nothing + emitter.instruction("mov r8, QWORD PTR [rdx]"); // load the replacement element count + emitter.instruction("test r8, r8"); // does the replacement contribute any elements? + emitter.instruction("jz __rt_asi_done_x86"); // an empty replacement inserts nothing + + // -- propagate the replacement's value_type so an empty destination is tagged -- + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // load the destination packed array kind word + emitter.instruction("mov r10, QWORD PTR [rdx - 8]"); // load the replacement packed array kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // materialize the destination preservation mask + emitter.instruction("and r9, r11"); // keep the heap marker, kind byte, and persistent COW bit + emitter.instruction("and r10, 0x7f00"); // keep only the replacement value_type lane + emitter.instruction("or r9, r10"); // combine the destination kind bits with the replacement tag + emitter.instruction("mov QWORD PTR [rdi - 8], r9"); // persist the propagated packed array value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the destination logical length + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the destination slot capacity + emitter.instruction("mov r11, r9"); // seed the required element count from the destination length + emitter.instruction("add r11, r8"); // compute the element count the insertion needs room for + emitter.label("__rt_asi_grow_check_x86"); + emitter.instruction("cmp r11, r10"); // does the destination already have room for the insertion? + emitter.instruction("jle __rt_asi_shift_x86"); // yes, start sliding the tail right + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination pointer before reallocating it + emitter.instruction("call __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // persist the possibly-relocated destination pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement pointer after the growth helper + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count after the growth helper + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the destination logical length after the growth helper + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // reload the destination capacity after the growth helper + emitter.instruction("lea r11, [r9 + r8]"); // recompute the element count the insertion needs room for + emitter.instruction("jmp __rt_asi_grow_check_x86"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asi_shift_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // r8 = replacement element count + emitter.instruction("mov r9, QWORD PTR [rdi]"); // r9 = destination logical length + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = requested insertion index + emitter.instruction("xor eax, eax"); // materialize zero as the insertion-index floor + emitter.instruction("test rsi, rsi"); // did the caller ask to insert before the first slot? + emitter.instruction("cmovs rsi, rax"); // clamp a negative insertion index to the front + emitter.instruction("cmp rsi, r9"); // does the insertion index lie past the last slot? + emitter.instruction("cmovg rsi, r9"); // clamp an over-large insertion index to an append + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // persist the clamped insertion index for the copy loop + emitter.instruction("lea r10, [rdi + 24]"); // r10 = destination payload base address + emitter.instruction("mov r11, r9"); // seed the slide cursor from the destination length + emitter.instruction("sub r11, 1"); // r11 = index of the last live destination element + emitter.label("__rt_asi_shift_loop_x86"); + emitter.instruction("cmp r11, rsi"); // have all elements at or after the insertion index moved? + emitter.instruction("jl __rt_asi_copy_x86"); // yes, write the replacement into the opened gap + emitter.instruction("mov rax, QWORD PTR [r10 + r11 * 8]"); // load the element that has to slide right + emitter.instruction("lea rcx, [r11 + r8]"); // compute its slot after the opened gap + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // store the element past the opened gap + emitter.instruction("sub r11, 1"); // walk backwards so overlapping slots stay intact + emitter.instruction("jmp __rt_asi_shift_loop_x86"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asi_copy_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("cmp rcx, r8"); // has every replacement element been copied? + emitter.instruction("jge __rt_asi_set_len_x86"); // yes, publish the extended destination length + emitter.instruction("lea r9, [rdx + 24]"); // compute the replacement payload base address + emitter.instruction("mov r10, QWORD PTR [r9 + rcx * 8]"); // load the borrowed replacement payload + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the clamped insertion index + emitter.instruction("lea r11, [rax + 24]"); // compute the destination payload base address + emitter.instruction("lea rsi, [rsi + rcx]"); // compute the destination slot for this replacement element + emitter.instruction("mov QWORD PTR [r11 + rsi * 8], r10"); // store the replacement payload into the opened gap + emitter.instruction("add rcx, 1"); // advance to the next replacement element + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // persist the updated replacement copy index + emitter.instruction("jmp __rt_asi_copy_x86"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asi_set_len_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the previous destination logical length + emitter.instruction("add r9, r8"); // extend it by the inserted element count + emitter.instruction("mov QWORD PTR [rax], r9"); // persist the extended destination logical length + + emitter.label("__rt_asi_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the possibly-relocated destination pointer + emitter.instruction("add rsp, 64"); // release the insertion bookkeeping spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_insert_refcounted`. +fn emit_array_splice_insert_refcounted_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_refcounted ---"); + emitter.label_global("__rt_array_splice_insert_refcounted"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before the insertion spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the insertion bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for destination, replacement, and indexes + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve the requested insertion index across growth + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the replacement copy loop at the first replacement slot + emitter.instruction("test rdx, rdx"); // did the caller pass a replacement indexed array at all? + emitter.instruction("jz __rt_asir_done_x86"); // a null replacement inserts nothing + emitter.instruction("mov r8, QWORD PTR [rdx]"); // load the replacement element count + emitter.instruction("test r8, r8"); // does the replacement contribute any elements? + emitter.instruction("jz __rt_asir_done_x86"); // an empty replacement inserts nothing + + // -- propagate the replacement's value_type so an empty destination is tagged -- + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // load the destination packed array kind word + emitter.instruction("mov r10, QWORD PTR [rdx - 8]"); // load the replacement packed array kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // materialize the destination preservation mask + emitter.instruction("and r9, r11"); // keep the heap marker, kind byte, and persistent COW bit + emitter.instruction("and r10, 0x7f00"); // keep only the replacement value_type lane + emitter.instruction("or r9, r10"); // combine the destination kind bits with the replacement tag + emitter.instruction("mov QWORD PTR [rdi - 8], r9"); // persist the propagated packed array value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the destination logical length + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the destination slot capacity + emitter.instruction("mov r11, r9"); // seed the required element count from the destination length + emitter.instruction("add r11, r8"); // compute the element count the insertion needs room for + emitter.label("__rt_asir_grow_check_x86"); + emitter.instruction("cmp r11, r10"); // does the destination already have room for the insertion? + emitter.instruction("jle __rt_asir_shift_x86"); // yes, start sliding the tail right + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination pointer before reallocating it + emitter.instruction("call __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // persist the possibly-relocated destination pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement pointer after the growth helper + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count after the growth helper + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the destination logical length after the growth helper + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // reload the destination capacity after the growth helper + emitter.instruction("lea r11, [r9 + r8]"); // recompute the element count the insertion needs room for + emitter.instruction("jmp __rt_asir_grow_check_x86"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asir_shift_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // r8 = replacement element count + emitter.instruction("mov r9, QWORD PTR [rdi]"); // r9 = destination logical length + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = requested insertion index + emitter.instruction("xor eax, eax"); // materialize zero as the insertion-index floor + emitter.instruction("test rsi, rsi"); // did the caller ask to insert before the first slot? + emitter.instruction("cmovs rsi, rax"); // clamp a negative insertion index to the front + emitter.instruction("cmp rsi, r9"); // does the insertion index lie past the last slot? + emitter.instruction("cmovg rsi, r9"); // clamp an over-large insertion index to an append + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // persist the clamped insertion index for the copy loop + emitter.instruction("lea r10, [rdi + 24]"); // r10 = destination payload base address + emitter.instruction("mov r11, r9"); // seed the slide cursor from the destination length + emitter.instruction("sub r11, 1"); // r11 = index of the last live destination element + emitter.label("__rt_asir_shift_loop_x86"); + emitter.instruction("cmp r11, rsi"); // have all elements at or after the insertion index moved? + emitter.instruction("jl __rt_asir_copy_x86"); // yes, write the replacement into the opened gap + emitter.instruction("mov rax, QWORD PTR [r10 + r11 * 8]"); // load the element that has to slide right + emitter.instruction("lea rcx, [r11 + r8]"); // compute its slot after the opened gap + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // store the element past the opened gap + emitter.instruction("sub r11, 1"); // walk backwards so overlapping slots stay intact + emitter.instruction("jmp __rt_asir_shift_loop_x86"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asir_copy_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("cmp rcx, r8"); // has every replacement element been copied? + emitter.instruction("jge __rt_asir_set_len_x86"); // yes, publish the extended destination length + emitter.instruction("lea r9, [rdx + 24]"); // compute the replacement payload base address + emitter.instruction("mov r10, QWORD PTR [r9 + rcx * 8]"); // load the borrowed replacement payload + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve the borrowed payload across the retain call + emitter.instruction("mov rax, r10"); // move the borrowed payload into the retain argument register + emitter.instruction("call __rt_incref"); // retain it before the destination array becomes an owner + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the retained payload after the retain call + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index after the retain call + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the clamped insertion index + emitter.instruction("lea r11, [rax + 24]"); // compute the destination payload base address + emitter.instruction("lea rsi, [rsi + rcx]"); // compute the destination slot for this replacement element + emitter.instruction("mov QWORD PTR [r11 + rsi * 8], r10"); // store the replacement payload into the opened gap + emitter.instruction("add rcx, 1"); // advance to the next replacement element + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // persist the updated replacement copy index + emitter.instruction("jmp __rt_asir_copy_x86"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asir_set_len_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the previous destination logical length + emitter.instruction("add r9, r8"); // extend it by the inserted element count + emitter.instruction("mov QWORD PTR [rax], r9"); // persist the extended destination logical length + + emitter.label("__rt_asir_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the possibly-relocated destination pointer + emitter.instruction("add rsp, 64"); // release the insertion bookkeeping spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_insert_boxed`. +fn emit_array_splice_insert_boxed_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_boxed ---"); + emitter.label_global("__rt_array_splice_insert_boxed"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before the insertion spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the insertion bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for destination, replacement, and indexes + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve the requested insertion index across growth + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the replacement copy loop at the first replacement slot + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // preserve the replacement slot value_type tag for the boxing calls + emitter.instruction("test rdx, rdx"); // did the caller pass a replacement indexed array at all? + emitter.instruction("jz __rt_asib_done_x86"); // a null replacement inserts nothing + emitter.instruction("mov r8, QWORD PTR [rdx]"); // load the replacement element count + emitter.instruction("test r8, r8"); // does the replacement contribute any elements? + emitter.instruction("jz __rt_asib_done_x86"); // an empty replacement inserts nothing + + // -- the destination stores boxed Mixed cells whatever the replacement slots hold -- + emitter.instruction("mov r9, QWORD PTR [rdi - 8]"); // load the destination packed array kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // materialize the destination preservation mask + emitter.instruction("and r9, r11"); // keep the heap marker, kind byte, and persistent COW bit + emitter.instruction("or r9, 0x700"); // runtime value_type tag 7 marks boxed Mixed payload slots + emitter.instruction("mov QWORD PTR [rdi - 8], r9"); // persist the boxed Mixed value_type tag + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the destination logical length + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the destination slot capacity + emitter.instruction("mov r11, r9"); // seed the required element count from the destination length + emitter.instruction("add r11, r8"); // compute the element count the insertion needs room for + emitter.label("__rt_asib_grow_check_x86"); + emitter.instruction("cmp r11, r10"); // does the destination already have room for the insertion? + emitter.instruction("jle __rt_asib_shift_x86"); // yes, start sliding the tail right + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination pointer before reallocating it + emitter.instruction("call __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // persist the possibly-relocated destination pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement pointer after the growth helper + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count after the growth helper + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the destination logical length after the growth helper + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // reload the destination capacity after the growth helper + emitter.instruction("lea r11, [r9 + r8]"); // recompute the element count the insertion needs room for + emitter.instruction("jmp __rt_asib_grow_check_x86"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asib_shift_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // r8 = replacement element count + emitter.instruction("mov r9, QWORD PTR [rdi]"); // r9 = destination logical length + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = requested insertion index + emitter.instruction("xor eax, eax"); // materialize zero as the insertion-index floor + emitter.instruction("test rsi, rsi"); // did the caller ask to insert before the first slot? + emitter.instruction("cmovs rsi, rax"); // clamp a negative insertion index to the front + emitter.instruction("cmp rsi, r9"); // does the insertion index lie past the last slot? + emitter.instruction("cmovg rsi, r9"); // clamp an over-large insertion index to an append + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // persist the clamped insertion index for the copy loop + emitter.instruction("lea r10, [rdi + 24]"); // r10 = destination payload base address + emitter.instruction("mov r11, r9"); // seed the slide cursor from the destination length + emitter.instruction("sub r11, 1"); // r11 = index of the last live destination element + emitter.label("__rt_asib_shift_loop_x86"); + emitter.instruction("cmp r11, rsi"); // have all elements at or after the insertion index moved? + emitter.instruction("jl __rt_asib_copy_x86"); // yes, write the replacement into the opened gap + emitter.instruction("mov rax, QWORD PTR [r10 + r11 * 8]"); // load the element that has to slide right + emitter.instruction("lea rcx, [r11 + r8]"); // compute its slot after the opened gap + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // store the element past the opened gap + emitter.instruction("sub r11, 1"); // walk backwards so overlapping slots stay intact + emitter.instruction("jmp __rt_asib_shift_loop_x86"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asib_copy_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("cmp rcx, r8"); // has every replacement element been copied? + emitter.instruction("jge __rt_asib_set_len_x86"); // yes, publish the extended destination length + emitter.instruction("lea r9, [rdx + 24]"); // compute the replacement payload base address + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the replacement slot value_type tag + emitter.instruction("cmp rax, 1"); // do the replacement slots hold string pointer/length pairs? + emitter.instruction("je __rt_asib_copy_string_x86"); // string slots need a wider load + emitter.instruction("mov rdi, QWORD PTR [r9 + rcx * 8]"); // load the raw replacement payload from its 8-byte slot + emitter.instruction("xor esi, esi"); // scalar Mixed payloads leave the high payload word clear + emitter.instruction("jmp __rt_asib_copy_box_x86"); // the payload words are ready for the Mixed cell allocator + emitter.label("__rt_asib_copy_string_x86"); + emitter.instruction("mov r11, rcx"); // seed the byte offset computation from the replacement index + emitter.instruction("shl r11, 4"); // each string slot is a 16-byte pointer/length pair + emitter.instruction("add r9, r11"); // advance to this element's string slot + emitter.instruction("mov rdi, QWORD PTR [r9]"); // load the borrowed string pointer from the replacement slot + emitter.instruction("mov rsi, QWORD PTR [r9 + 8]"); // load the borrowed string length from the replacement slot + emitter.label("__rt_asib_copy_box_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // pass the replacement slot value_type tag to the boxer + emitter.instruction("call __rt_mixed_from_value"); // allocate one owned Mixed cell for this element + emitter.instruction("mov r10, rax"); // the fresh Mixed cell is what the destination slot receives + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index after the boxing call + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the clamped insertion index + emitter.instruction("lea r11, [rax + 24]"); // compute the destination payload base address + emitter.instruction("lea rsi, [rsi + rcx]"); // compute the destination slot for this replacement element + emitter.instruction("mov QWORD PTR [r11 + rsi * 8], r10"); // store the replacement payload into the opened gap + emitter.instruction("add rcx, 1"); // advance to the next replacement element + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // persist the updated replacement copy index + emitter.instruction("jmp __rt_asib_copy_x86"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asib_set_len_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the previous destination logical length + emitter.instruction("add r9, r8"); // extend it by the inserted element count + emitter.instruction("mov QWORD PTR [rax], r9"); // persist the extended destination logical length + + emitter.label("__rt_asib_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the possibly-relocated destination pointer + emitter.instruction("add rsp, 64"); // release the insertion bookkeeping spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_insert_unboxed`. +fn emit_array_splice_insert_unboxed_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_unboxed ---"); + emitter.label_global("__rt_array_splice_insert_unboxed"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before the insertion spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the insertion bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for destination, replacement, and indexes + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve the requested insertion index across growth + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the replacement copy loop at the first replacement slot + emitter.instruction("test rdx, rdx"); // did the caller pass a replacement indexed array at all? + emitter.instruction("jz __rt_asiu_done_x86"); // a null replacement inserts nothing + emitter.instruction("mov r8, QWORD PTR [rdx]"); // load the replacement element count + emitter.instruction("test r8, r8"); // does the replacement contribute any elements? + emitter.instruction("jz __rt_asiu_done_x86"); // an empty replacement inserts nothing + + // -- grow the destination until the inserted elements fit its payload -- + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the destination logical length + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the destination slot capacity + emitter.instruction("mov r11, r9"); // seed the required element count from the destination length + emitter.instruction("add r11, r8"); // compute the element count the insertion needs room for + emitter.label("__rt_asiu_grow_check_x86"); + emitter.instruction("cmp r11, r10"); // does the destination already have room for the insertion? + emitter.instruction("jle __rt_asiu_shift_x86"); // yes, start sliding the tail right + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination pointer before reallocating it + emitter.instruction("call __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // persist the possibly-relocated destination pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement pointer after the growth helper + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count after the growth helper + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the destination logical length after the growth helper + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // reload the destination capacity after the growth helper + emitter.instruction("lea r11, [r9 + r8]"); // recompute the element count the insertion needs room for + emitter.instruction("jmp __rt_asiu_grow_check_x86"); // keep growing until the insertion fits + + // -- slide the elements at and after the insertion index to the right -- + emitter.label("__rt_asiu_shift_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // r8 = replacement element count + emitter.instruction("mov r9, QWORD PTR [rdi]"); // r9 = destination logical length + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = requested insertion index + emitter.instruction("xor eax, eax"); // materialize zero as the insertion-index floor + emitter.instruction("test rsi, rsi"); // did the caller ask to insert before the first slot? + emitter.instruction("cmovs rsi, rax"); // clamp a negative insertion index to the front + emitter.instruction("cmp rsi, r9"); // does the insertion index lie past the last slot? + emitter.instruction("cmovg rsi, r9"); // clamp an over-large insertion index to an append + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // persist the clamped insertion index for the copy loop + emitter.instruction("lea r10, [rdi + 24]"); // r10 = destination payload base address + emitter.instruction("mov r11, r9"); // seed the slide cursor from the destination length + emitter.instruction("sub r11, 1"); // r11 = index of the last live destination element + emitter.label("__rt_asiu_shift_loop_x86"); + emitter.instruction("cmp r11, rsi"); // have all elements at or after the insertion index moved? + emitter.instruction("jl __rt_asiu_copy_x86"); // yes, write the replacement into the opened gap + emitter.instruction("mov rax, QWORD PTR [r10 + r11 * 8]"); // load the element that has to slide right + emitter.instruction("lea rcx, [r11 + r8]"); // compute its slot after the opened gap + emitter.instruction("mov QWORD PTR [r10 + rcx * 8], rax"); // store the element past the opened gap + emitter.instruction("sub r11, 1"); // walk backwards so overlapping slots stay intact + emitter.instruction("jmp __rt_asiu_shift_loop_x86"); // continue sliding the tail right + + // -- copy the replacement payloads into the opened gap -- + emitter.label("__rt_asiu_copy_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("cmp rcx, r8"); // has every replacement element been copied? + emitter.instruction("jge __rt_asiu_set_len_x86"); // yes, publish the extended destination length + emitter.instruction("lea r9, [rdx + 24]"); // compute the replacement payload base address + emitter.instruction("mov r10, QWORD PTR [r9 + rcx * 8]"); // load the borrowed replacement payload + emitter.instruction("mov rax, r10"); // move the boxed Mixed cell into the x86_64 unbox input register + emitter.instruction("call __rt_mixed_cast_int"); // read the cell's integer payload for the typed slot + emitter.instruction("mov r10, rax"); // the plain integer is what the destination slot receives + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index after the unbox call + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the clamped insertion index + emitter.instruction("lea r11, [rax + 24]"); // compute the destination payload base address + emitter.instruction("lea rsi, [rsi + rcx]"); // compute the destination slot for this replacement element + emitter.instruction("mov QWORD PTR [r11 + rsi * 8], r10"); // store the replacement payload into the opened gap + emitter.instruction("add rcx, 1"); // advance to the next replacement element + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // persist the updated replacement copy index + emitter.instruction("jmp __rt_asiu_copy_x86"); // continue copying replacement elements + + // -- publish the extended destination length -- + emitter.label("__rt_asiu_set_len_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r8, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r9, QWORD PTR [rax]"); // reload the previous destination logical length + emitter.instruction("add r9, r8"); // extend it by the inserted element count + emitter.instruction("mov QWORD PTR [rax], r9"); // persist the extended destination logical length + + emitter.label("__rt_asiu_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the possibly-relocated destination pointer + emitter.instruction("add rsp, 64"); // release the insertion bookkeeping spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} diff --git a/src/codegen_support/runtime/arrays/array_splice_refcounted.rs b/src/codegen_support/runtime/arrays/array_splice_refcounted.rs index 72e8a1706e..efe04d3225 100644 --- a/src/codegen_support/runtime/arrays/array_splice_refcounted.rs +++ b/src/codegen_support/runtime/arrays/array_splice_refcounted.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_array_splice_refcounted`, `__rt_array_new` runtime helper assembly for array splice refcounted. +//! Emits the `__rt_array_splice_refcounted` runtime helper assembly for array splice refcounted. //! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,9 +7,12 @@ //! //! Key details: //! - Array helpers operate on runtime array headers and element cells; mutations must respect capacity and COW contracts. +//! - The removal window is normalized by the shared `slice_bounds` prologue, so the removal count is +//! always non-negative and the compaction loop never reads or writes outside the source payload. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; /// Emits the `__rt_array_splice_refcounted` runtime helper for array splice. /// @@ -22,14 +25,17 @@ use crate::codegen_support::platform::Arch; /// /// # Input registers (ARM64 calling convention) /// * `x0` - source array pointer -/// * `x1` - splice offset (starting position of removal) -/// * `x2` - removal length; `-1` means "remove everything from offset to the end" +/// * `x1` - `$offset` (starting position of removal, may be negative) +/// * `x2` - `$length` (may be negative) +/// * `x3` - 1 when a `$length` was supplied, 0 when it was omitted or `null` /// /// # Output registers (ARM64 calling convention) /// * `x0` - new array containing the removed elements (caller owns) +/// * `x1` - the normalized removal offset, i.e. the index a `$replacement` is inserted at /// /// # ABI details -/// * Clamps the removal length so it never exceeds the remaining elements. +/// * `emit_slice_bounds` normalizes the offset/length pair first, so the removal count is always in +/// `[0, array_length - offset]`. /// * Preserves source array metadata; updates the source array's logical length in-place. /// * Calls `__rt_array_new` and `__rt_array_push_refcounted` helpers. pub fn emit_array_splice_refcounted(emitter: &mut Emitter) { @@ -47,14 +53,10 @@ pub fn emit_array_splice_refcounted(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("add x29, sp, #32"); // set up new frame pointer emitter.instruction("str x0, [sp, #0]"); // save source array pointer - emitter.instruction("str x1, [sp, #8]"); // save offset - emitter.instruction("str x2, [sp, #16]"); // save removal length - - // -- clamp removal length to not exceed array bounds -- - emitter.instruction("ldr x3, [x0]"); // load source array length - emitter.instruction("sub x4, x3, x1"); // compute maximum removable length - emitter.instruction("cmp x2, x4"); // compare requested length with maximum removable length - emitter.instruction("csel x2, x4, x2, gt"); // clamp length to the remaining number of elements + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice_ref"); + emitter.instruction("str x1, [sp, #8]"); // save normalized offset emitter.instruction("str x2, [sp, #16]"); // save clamped removal length // -- create result array for removed elements -- @@ -106,6 +108,7 @@ pub fn emit_array_splice_refcounted(emitter: &mut Emitter) { // -- return removed-elements result array -- emitter.instruction("ldr x0, [sp, #24]"); // reload result array pointer + emitter.instruction("ldr x1, [sp, #8]"); // return the normalized removal offset, the index a $replacement is inserted at emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #48"); // deallocate stack frame emitter.instruction("ret"); // return result array @@ -114,10 +117,12 @@ pub fn emit_array_splice_refcounted(emitter: &mut Emitter) { /// Emits the x86_64 Linux variant of `__rt_array_splice_refcounted`. /// /// Identical in behavior to the ARM64 variant but emits x86_64 instructions using the -/// System V AMD64 ABI (registers: rdi=array, rsi=offset, rdx=length; return in rax). +/// System V AMD64 ABI (registers: rdi=array, rsi=`$offset`, rdx=`$length`, rcx=1 when a `$length` +/// was supplied and 0 when it was omitted or `null`; returns the removed-elements array in rax and +/// the normalized removal offset in rdx). /// -/// The implementation mirrors the ARM64 logic: clamp length, copy removed elements into a -/// new result array, shift remaining elements left in-place, and return the result array. +/// The implementation mirrors the ARM64 logic: normalize the removal window, copy removed elements +/// into a new result array, shift remaining elements left in-place, and return the result array. fn emit_array_splice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: array_splice_refcounted ---"); @@ -127,20 +132,10 @@ fn emit_array_splice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source indexed-array pointer, clamped removal length, and removed-elements result array emitter.instruction("sub rsp, 48"); // reserve aligned spill slots for the refcounted splice bookkeeping while keeping helper calls 16-byte aligned emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source indexed-array pointer across removal-length clamping and result-array construction - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the requested splice offset across the result-array constructor call and later compaction loop - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the source indexed-array logical length before clamping the requested removal length - emitter.instruction("mov rcx, r10"); // seed the remaining-window scratch register from the source indexed-array logical length - emitter.instruction("sub rcx, rsi"); // compute the maximum removable refcounted payload count from the requested splice offset - emitter.instruction("cmp rdx, -1"); // detect the sentinel that means array_splice should remove until the end of the source indexed array - emitter.instruction("jne __rt_array_splice_ref_known_len_x86"); // keep the explicit requested removal length when the caller did not use the until-end sentinel - emitter.instruction("mov rdx, rcx"); // replace the until-end sentinel with the remaining refcounted payload count in the source indexed array - - emitter.label("__rt_array_splice_ref_known_len_x86"); - emitter.instruction("cmp rdx, rcx"); // clamp the requested removal length so it never extends beyond the source indexed-array bounds - emitter.instruction("jle __rt_array_splice_ref_len_ready_x86"); // keep the explicit requested removal length when it already fits inside the remaining refcounted payload window - emitter.instruction("mov rdx, rcx"); // clamp the requested removal length down to the maximum removable refcounted payload count - - emitter.label("__rt_array_splice_ref_len_ready_x86"); + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice_ref"); + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the normalized splice offset across the result-array constructor call and later compaction loop emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // preserve the clamped removal length across the result-array constructor call and later compaction loop emitter.instruction("mov rdi, rdx"); // pass the clamped removal length as the removed-elements result capacity to the shared constructor emitter.instruction("mov rsi, 8"); // request 8-byte payload slots for the removed-elements result indexed array @@ -193,6 +188,7 @@ fn emit_array_splice_refcounted_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub r11, r9"); // compute the shortened source indexed-array logical length after removing the splice window emitter.instruction("mov QWORD PTR [r10], r11"); // persist the shortened source indexed-array logical length back into the array header emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the removed-elements result indexed-array pointer before returning it to the caller + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // return the normalized removal offset, the index a $replacement is inserted at emitter.instruction("add rsp, 48"); // release the refcounted splice spill slots before returning emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the removed-elements result indexed-array pointer in rax diff --git a/src/codegen_support/runtime/arrays/array_splice_str.rs b/src/codegen_support/runtime/arrays/array_splice_str.rs new file mode 100644 index 0000000000..75a99a4674 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_splice_str.rs @@ -0,0 +1,486 @@ +//! Purpose: +//! Emits `__rt_array_splice_str` and `__rt_array_splice_insert_str`, the `array_splice()` runtime +//! helpers for indexed arrays whose payload slots are string `{pointer, length}` pairs. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! - The EIR lowering of `array_splice()` in `crate::codegen::lower_inst::builtins::arrays`. +//! +//! Key details: +//! - Indexed string arrays use 16-byte slots (`__rt_array_new(n, 16)`), not the 8-byte slots the +//! scalar and refcounted splice helpers move. Running those over a string array copied one word +//! per element, so the removed-elements array came back holding raw pointers read as integers +//! and the source array kept a half-shifted payload. +//! - The removal MOVES each string slot into the result. An indexed string array owns its +//! persisted payloads exclusively (`__rt_array_clone_shallow` re-persists them on a +//! copy-on-write split and `__rt_array_free_deep` frees each one), so transferring the pointer +//! keeps exactly one owner per string. Retaining or copying here would double-free or leak. +//! - The insertion DUPLICATES each replacement string through `__rt_str_persist`, because the +//! replacement array keeps owning its own payloads and is released by the caller afterwards. +//! - The destination is grown before anything is written, and the tail slide walks backwards, so +//! a replacement longer than the removed window never overwrites a slot it has not moved yet. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::arrays::slice_bounds::emit_slice_bounds; + +/// Emits the `__rt_array_splice_str` runtime helper for the active target. +/// +/// Removes `$length` string slots starting at `$offset`, returns them in a freshly allocated +/// 16-byte-slot indexed array, and compacts the source payload over the gap. +/// +/// ## ARM64 ABI +/// - **Input**: `x0` = source indexed array, `x1` = `$offset`, `x2` = `$length`, `x3` = 1 when a +/// `$length` was supplied and 0 when it was omitted or `null` +/// - **Output**: `x0` = the removed-elements array, `x1` = the normalized removal offset, i.e. +/// the index a `$replacement` is inserted at +/// +/// ## x86_64 ABI +/// - **Input**: `rdi`, `rsi`, `rdx`, `rcx` with the same meaning +/// - **Output**: `rax` = the removed-elements array, `rdx` = the normalized removal offset +/// +/// The window is normalized by the shared `emit_slice_bounds` prologue, so the removal count is +/// always in `[0, length - offset]` and neither loop can step outside the source payload. +pub fn emit_array_splice_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_str_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_str ---"); + emitter.label_global("__rt_array_splice_str"); + + // Stack layout: [sp,#0] source array, [sp,#8] normalized offset, [sp,#16] removal count, + // [sp,#24] result array, [sp,#32] saved x29/x30. + emitter.instruction("sub sp, sp, #48"); // reserve the string-splice bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the source indexed-array pointer across the result-array constructor + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice_str"); + emitter.instruction("str x1, [sp, #8]"); // save the normalized removal offset + emitter.instruction("str x2, [sp, #16]"); // save the clamped removal count + + // -- allocate the removed-elements array with string-shaped 16-byte payload slots -- + emitter.instruction("mov x0, x2"); // capacity = removal count + emitter.instruction("mov x1, #16"); // string payload slots carry a pointer and a length + emitter.instruction("bl __rt_array_new"); // allocate the removed-elements array, already stamped as a string array + emitter.instruction("str x0, [sp, #24]"); // preserve the removed-elements array pointer across the move loops + + // -- move the removed string slots out of the source and into the result -- + emitter.instruction("ldr x3, [sp, #0]"); // reload the source indexed-array pointer + emitter.instruction("add x3, x3, #24"); // compute the source string payload base address + emitter.instruction("add x4, x0, #24"); // compute the result string payload base address + emitter.instruction("ldr x5, [sp, #8]"); // reload the normalized removal offset + emitter.instruction("ldr x6, [sp, #16]"); // reload the clamped removal count + emitter.instruction("mov x7, #0"); // start the move loop at the first removed string slot + + emitter.label("__rt_array_splice_str_copy"); + emitter.instruction("cmp x7, x6"); // has every removed string slot been moved out? + emitter.instruction("b.ge __rt_array_splice_str_copy_done"); // finish once the removed window is materialized + emitter.instruction("add x8, x5, x7"); // compute the source slot index inside the removed window + emitter.instruction("lsl x8, x8, #4"); // scale it by the 16-byte string slot size + emitter.instruction("add x8, x3, x8"); // compute the source string slot address + emitter.instruction("ldp x9, x10, [x8]"); // load the removed string pointer and length + emitter.instruction("lsl x11, x7, #4"); // scale the result cursor by the 16-byte string slot size + emitter.instruction("add x11, x4, x11"); // compute the destination string slot address + emitter.instruction("stp x9, x10, [x11]"); // hand the owned string payload over to the result array + emitter.instruction("add x7, x7, #1"); // advance to the next removed string slot + emitter.instruction("b __rt_array_splice_str_copy"); // keep moving removed string slots + + emitter.label("__rt_array_splice_str_copy_done"); + emitter.instruction("ldr x0, [sp, #24]"); // reload the removed-elements array pointer + emitter.instruction("ldr x6, [sp, #16]"); // reload the clamped removal count + emitter.instruction("str x6, [x0]"); // publish the removed-elements array logical length + + // -- compact the surviving source string slots over the removed window -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the source indexed-array pointer + emitter.instruction("ldr x12, [x0]"); // load the original source logical length + emitter.instruction("add x3, x0, #24"); // recompute the source string payload base address + emitter.instruction("ldr x5, [sp, #8]"); // seed the destination compaction cursor from the removal offset + emitter.instruction("ldr x6, [sp, #16]"); // reload the clamped removal count + emitter.instruction("add x7, x5, x6"); // seed the source compaction cursor past the removed window + + emitter.label("__rt_array_splice_str_shift"); + emitter.instruction("cmp x7, x12"); // has every trailing string slot slid left? + emitter.instruction("b.ge __rt_array_splice_str_update"); // stop once the source gap is closed + emitter.instruction("lsl x8, x7, #4"); // scale the trailing source cursor by the string slot size + emitter.instruction("add x8, x3, x8"); // compute the trailing source string slot address + emitter.instruction("ldp x9, x10, [x8]"); // load the trailing string pointer and length + emitter.instruction("lsl x11, x5, #4"); // scale the compacted destination cursor by the string slot size + emitter.instruction("add x11, x3, x11"); // compute the compacted destination string slot address + emitter.instruction("stp x9, x10, [x11]"); // slide the trailing string payload left over the removed window + emitter.instruction("add x5, x5, #1"); // advance the compacted destination cursor + emitter.instruction("add x7, x7, #1"); // advance the trailing source cursor + emitter.instruction("b __rt_array_splice_str_shift"); // keep compacting trailing string slots + + emitter.label("__rt_array_splice_str_update"); + emitter.instruction("sub x12, x12, x6"); // compute the shortened source logical length + emitter.instruction("str x12, [x0]"); // persist the shortened source logical length + emitter.instruction("ldr x0, [sp, #24]"); // return the removed-elements array pointer + emitter.instruction("ldr x1, [sp, #8]"); // return the normalized removal offset, the index a $replacement is inserted at + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-splice bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_str`. +/// +/// Mirrors the ARM64 sequence instruction for instruction; only the register encoding differs. +/// See [`emit_array_splice_str`] for the full ABI and the ownership contract. +fn emit_array_splice_str_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_str ---"); + emitter.label_global("__rt_array_splice_str"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving the string-splice spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the source, window, and result bookkeeping + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots while keeping the constructor call 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the source indexed-array pointer across the result-array constructor + + // -- normalize the requested removal window against PHP's offset/length rules -- + emit_slice_bounds(emitter, "__rt_array_splice_str"); + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the normalized removal offset + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the clamped removal count + emitter.instruction("mov rdi, rdx"); // pass the removal count as the removed-elements array capacity + emitter.instruction("mov rsi, 16"); // request string-shaped 16-byte payload slots + emitter.instruction("call __rt_array_new"); // allocate the removed-elements array, already stamped as a string array + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the removed-elements array pointer across the move loops + + // -- move the removed string slots out of the source and into the result -- + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source indexed-array pointer after the constructor + emitter.instruction("lea r10, [r10 + 24]"); // compute the source string payload base address + emitter.instruction("lea r11, [rax + 24]"); // compute the result string payload base address + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // seed the source cursor from the normalized removal offset + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the clamped removal count + emitter.instruction("xor ecx, ecx"); // start the move loop at the first removed string slot + + emitter.label("__rt_array_splice_str_copy_x86"); + emitter.instruction("cmp rcx, r9"); // has every removed string slot been moved out? + emitter.instruction("jge __rt_array_splice_str_copy_done_x86"); // finish once the removed window is materialized + emitter.instruction("mov rax, r8"); // copy the source cursor before scaling it + emitter.instruction("shl rax, 4"); // scale the source cursor by the 16-byte string slot size + emitter.instruction("mov rdx, QWORD PTR [r10 + rax]"); // load the removed string pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + rax + 8]"); // load the removed string length + emitter.instruction("mov rax, rcx"); // copy the result cursor before scaling it + emitter.instruction("shl rax, 4"); // scale the result cursor by the 16-byte string slot size + emitter.instruction("mov QWORD PTR [r11 + rax], rdx"); // hand the owned string pointer over to the result array + emitter.instruction("mov QWORD PTR [r11 + rax + 8], rsi"); // store the matching string length in the result slot + emitter.instruction("add r8, 1"); // advance to the next removed string slot + emitter.instruction("add rcx, 1"); // advance the result cursor + emitter.instruction("jmp __rt_array_splice_str_copy_x86"); // keep moving removed string slots + + emitter.label("__rt_array_splice_str_copy_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the removed-elements array pointer + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the clamped removal count + emitter.instruction("mov QWORD PTR [rax], r9"); // publish the removed-elements array logical length + + // -- compact the surviving source string slots over the removed window -- + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source indexed-array pointer + emitter.instruction("mov rdi, QWORD PTR [r10]"); // load the original source logical length + emitter.instruction("lea r10, [r10 + 24]"); // recompute the source string payload base address + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // seed the destination compaction cursor from the removal offset + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the clamped removal count + emitter.instruction("add r9, r8"); // seed the source compaction cursor past the removed window + + emitter.label("__rt_array_splice_str_shift_x86"); + emitter.instruction("cmp r9, rdi"); // has every trailing string slot slid left? + emitter.instruction("jge __rt_array_splice_str_update_x86"); // stop once the source gap is closed + emitter.instruction("mov rax, r9"); // copy the trailing source cursor before scaling it + emitter.instruction("shl rax, 4"); // scale it by the 16-byte string slot size + emitter.instruction("mov rdx, QWORD PTR [r10 + rax]"); // load the trailing string pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + rax + 8]"); // load the trailing string length + emitter.instruction("mov rax, r8"); // copy the compacted destination cursor before scaling it + emitter.instruction("shl rax, 4"); // scale it by the 16-byte string slot size + emitter.instruction("mov QWORD PTR [r10 + rax], rdx"); // slide the trailing string pointer left over the removed window + emitter.instruction("mov QWORD PTR [r10 + rax + 8], rsi"); // slide the matching string length left as well + emitter.instruction("add r8, 1"); // advance the compacted destination cursor + emitter.instruction("add r9, 1"); // advance the trailing source cursor + emitter.instruction("jmp __rt_array_splice_str_shift_x86"); // keep compacting trailing string slots + + emitter.label("__rt_array_splice_str_update_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source indexed-array pointer + emitter.instruction("mov r11, QWORD PTR [r10]"); // reload the original source logical length + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the clamped removal count + emitter.instruction("sub r11, r9"); // compute the shortened source logical length + emitter.instruction("mov QWORD PTR [r10], r11"); // persist the shortened source logical length + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the removed-elements array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // return the normalized removal offset, the index a $replacement is inserted at + emitter.instruction("add rsp, 32"); // release the string-splice spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} + +/// Emits the `__rt_array_splice_insert_str` runtime helper for the active target. +/// +/// Writes `$replacement`'s strings into the 16-byte-slot gap the removal opened, duplicating each +/// payload with `__rt_str_persist` so the destination and the replacement array each own their own +/// bytes. +/// +/// ## ARM64 ABI +/// - **Input**: `x0` = destination indexed array, `x1` = insertion index, `x2` = replacement +/// indexed array (0 inserts nothing) +/// - **Output**: `x0` = the possibly-relocated destination indexed array +/// +/// ## x86_64 ABI +/// - **Input**: `rdi` = destination, `rsi` = insertion index, `rdx` = replacement +/// - **Output**: `rax` = the possibly-relocated destination indexed array +/// +/// A destination that is still empty carries the 8-byte `array` shape its literal was +/// allocated with, so the first write re-scales the capacity into 16-byte slots and stamps the +/// string `value_type` exactly like `__rt_array_push_str` does. The insertion index is clamped to +/// `[0, length]`, so an unnormalized offset still cannot write outside the payload. +pub fn emit_array_splice_insert_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_splice_insert_str_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_str ---"); + emitter.label_global("__rt_array_splice_insert_str"); + + // Stack layout: [sp,#0] destination array, [sp,#8] replacement array, + // [sp,#16] insertion index, [sp,#24] copy loop index, [sp,#48] saved x29/x30. + emitter.instruction("sub sp, sp, #64"); // reserve the insertion bookkeeping slots plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // preserve the destination indexed-array pointer across growth + emitter.instruction("str x2, [sp, #8]"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("str x1, [sp, #16]"); // preserve the requested insertion index across growth + emitter.instruction("str xzr, [sp, #24]"); // start the replacement copy loop at the first replacement slot + emitter.instruction("cbz x2, __rt_asis_done"); // a null replacement inserts nothing + emitter.instruction("ldr x9, [x2]"); // x9 = replacement element count + emitter.instruction("cbz x9, __rt_asis_done"); // an empty replacement inserts nothing + + // -- specialize a still-empty destination to 16-byte string slots before the first write -- + emitter.instruction("ldr x10, [x0]"); // load the destination logical length + emitter.instruction("cbnz x10, __rt_asis_shape_ready"); // a non-empty destination already has its string shape fixed + emitter.instruction("ldr x11, [x0, #16]"); // x11 = old elem_size (8 for an empty array buffer) + emitter.instruction("ldr x12, [x0, #8]"); // x12 = old capacity counted in old-elem_size slots + emitter.instruction("mul x12, x12, x11"); // x12 = backing-store data bytes already reserved + emitter.instruction("lsr x12, x12, #4"); // reinterpret the same bytes as 16-byte string slots + emitter.instruction("str x12, [x0, #8]"); // publish slot-accurate capacity before any 16-byte write + emitter.instruction("mov x11, #16"); // string payload slots carry a pointer and a length + emitter.instruction("str x11, [x0, #16]"); // elem_size = 16 before any future grow copies live string slots + emitter.label("__rt_asis_shape_ready"); + + // -- the destination stores string pointer/length pairs whatever it held before -- + emitter.instruction("ldr x10, [x0, #-8]"); // load the destination packed array kind word + emitter.instruction("mov x12, #0x80ff"); // preserve the destination kind byte and persistent COW flag + emitter.instruction("and x10, x10, x12"); // drop stale destination value_type bits before restamping + emitter.instruction("mov x11, #0x100"); // runtime value_type tag 1 marks string payload slots + emitter.instruction("orr x10, x10, x11"); // combine the destination kind bits with the string value_type tag + emitter.instruction("str x10, [x0, #-8]"); // persist the string value_type tag + + // -- grow the destination until the inserted string slots fit its payload -- + emitter.label("__rt_asis_grow_check"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the destination logical length + emitter.instruction("ldr x11, [x0, #8]"); // reload the destination slot capacity + emitter.instruction("add x10, x10, x9"); // compute the element count the insertion needs room for + emitter.instruction("cmp x10, x11"); // does the destination already have room for the insertion? + emitter.instruction("b.le __rt_asis_shift"); // yes, start sliding the tail right + emitter.instruction("bl __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("str x0, [sp, #0]"); // persist the possibly-relocated destination pointer + emitter.instruction("b __rt_asis_grow_check"); // keep growing until the insertion fits + + // -- slide the string slots at and after the insertion index to the right -- + emitter.label("__rt_asis_shift"); + emitter.instruction("ldr x1, [sp, #16]"); // x1 = requested insertion index + emitter.instruction("ldr x10, [x0]"); // x10 = destination logical length + emitter.instruction("cmp x1, #0"); // did the caller ask to insert before the first slot? + emitter.instruction("csel x1, x1, xzr, ge"); // clamp a negative insertion index to the front + emitter.instruction("cmp x1, x10"); // does the insertion index lie past the last slot? + emitter.instruction("csel x1, x1, x10, lt"); // clamp an over-large insertion index to an append + emitter.instruction("str x1, [sp, #16]"); // persist the clamped insertion index for the copy loop + emitter.instruction("add x3, x0, #24"); // x3 = destination string payload base address + emitter.instruction("sub x4, x10, #1"); // x4 = index of the last live destination string slot + emitter.label("__rt_asis_shift_loop"); + emitter.instruction("cmp x4, x1"); // have all slots at or after the insertion index moved? + emitter.instruction("b.lt __rt_asis_copy"); // yes, write the replacement into the opened gap + emitter.instruction("lsl x5, x4, #4"); // scale the source slot index by the 16-byte string slot size + emitter.instruction("add x5, x3, x5"); // compute the source string slot address + emitter.instruction("ldp x6, x7, [x5]"); // load the string pointer/length pair that has to slide right + emitter.instruction("add x8, x4, x9"); // compute its slot after the opened gap + emitter.instruction("lsl x8, x8, #4"); // scale that slot index by the string slot size + emitter.instruction("add x8, x3, x8"); // compute the destination string slot address + emitter.instruction("stp x6, x7, [x8]"); // store the pair past the opened gap + emitter.instruction("sub x4, x4, #1"); // walk backwards so overlapping slots stay intact + emitter.instruction("b __rt_asis_shift_loop"); // continue sliding the tail right + + // -- duplicate the replacement strings into the opened gap -- + emitter.label("__rt_asis_copy"); + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("cmp x4, x9"); // has every replacement string been copied? + emitter.instruction("b.ge __rt_asis_set_len"); // yes, publish the extended destination length + emitter.instruction("add x5, x2, #24"); // compute the replacement string payload base address + emitter.instruction("lsl x6, x4, #4"); // scale the copy index by the 16-byte string slot size + emitter.instruction("add x5, x5, x6"); // compute this element's replacement string slot address + emitter.instruction("ldr x1, [x5]"); // load the borrowed replacement string pointer + emitter.instruction("ldr x2, [x5, #8]"); // load the borrowed replacement string length + emitter.instruction("bl __rt_str_persist"); // duplicate it so the destination owns its own bytes + emitter.instruction("ldr x4, [sp, #24]"); // reload the replacement copy index after the persist call + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x3, [sp, #16]"); // reload the clamped insertion index + emitter.instruction("add x5, x0, #24"); // compute the destination string payload base address + emitter.instruction("add x6, x3, x4"); // compute the destination slot for this replacement element + emitter.instruction("lsl x6, x6, #4"); // scale that slot index by the string slot size + emitter.instruction("add x6, x5, x6"); // compute the destination string slot address + emitter.instruction("str x1, [x6]"); // store the owned string pointer into the opened gap + emitter.instruction("str x2, [x6, #8]"); // store the matching owned string length + emitter.instruction("add x4, x4, #1"); // advance to the next replacement element + emitter.instruction("str x4, [sp, #24]"); // persist the updated replacement copy index + emitter.instruction("b __rt_asis_copy"); // continue copying replacement strings + + // -- publish the extended destination length -- + emitter.label("__rt_asis_set_len"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination indexed-array pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the replacement indexed-array pointer + emitter.instruction("ldr x9, [x2]"); // reload the replacement element count + emitter.instruction("ldr x10, [x0]"); // reload the previous destination logical length + emitter.instruction("add x10, x10, x9"); // extend it by the inserted element count + emitter.instruction("str x10, [x0]"); // persist the extended destination logical length + + emitter.label("__rt_asis_done"); + emitter.instruction("ldr x0, [sp, #0]"); // return the possibly-relocated destination pointer + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the insertion bookkeeping slots + emitter.instruction("ret"); // return to the caller +} + +/// Emits the x86_64 System V variant of `__rt_array_splice_insert_str`. +/// +/// Mirrors the ARM64 sequence; only the register encoding differs. Note that `__rt_str_persist` +/// takes its source pointer in `rax` (not the first SysV argument register) and returns the owned +/// pointer in `rax` with the length in `rdx`. +fn emit_array_splice_insert_str_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_splice_insert_str ---"); + emitter.label_global("__rt_array_splice_insert_str"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before the insertion spill slots + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the insertion bookkeeping + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for destination, replacement, and indexes + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the destination indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // preserve the replacement indexed-array pointer across growth + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // preserve the requested insertion index across growth + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the replacement copy loop at the first replacement slot + emitter.instruction("test rdx, rdx"); // is the replacement pointer null? + emitter.instruction("jz __rt_asis_done_x86"); // a null replacement inserts nothing + emitter.instruction("mov r9, QWORD PTR [rdx]"); // r9 = replacement element count + emitter.instruction("test r9, r9"); // is the replacement array empty? + emitter.instruction("jz __rt_asis_done_x86"); // an empty replacement inserts nothing + + // -- specialize a still-empty destination to 16-byte string slots before the first write -- + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the destination logical length + emitter.instruction("test r10, r10"); // is this the first write into a still-empty destination? + emitter.instruction("jnz __rt_asis_shape_ready_x86"); // a non-empty destination already has its string shape fixed + emitter.instruction("mov r10, QWORD PTR [rdi + 16]"); // r10 = old elem_size (8 for an empty array buffer) + emitter.instruction("mov r11, QWORD PTR [rdi + 8]"); // r11 = old capacity counted in old-elem_size slots + emitter.instruction("imul r11, r10"); // r11 = backing-store data bytes already reserved + emitter.instruction("shr r11, 4"); // reinterpret the same bytes as 16-byte string slots + emitter.instruction("mov QWORD PTR [rdi + 8], r11"); // publish slot-accurate capacity before any 16-byte write + emitter.instruction("mov QWORD PTR [rdi + 16], 16"); // elem_size = 16 before any future grow copies live string slots + emitter.label("__rt_asis_shape_ready_x86"); + + // -- the destination stores string pointer/length pairs whatever it held before -- + emitter.instruction("mov r10, QWORD PTR [rdi - 8]"); // load the destination packed array kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // preserve heap marker, indexed-array kind, and persistent COW metadata + emitter.instruction("and r10, r11"); // drop stale destination value_type bits before restamping + emitter.instruction("or r10, 0x100"); // runtime value_type tag 1 marks string payload slots + emitter.instruction("mov QWORD PTR [rdi - 8], r10"); // persist the string value_type tag + + // -- grow the destination until the inserted string slots fit its payload -- + emitter.label("__rt_asis_grow_check_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r9, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r10, QWORD PTR [rdi]"); // reload the destination logical length + emitter.instruction("mov r11, QWORD PTR [rdi + 8]"); // reload the destination slot capacity + emitter.instruction("add r10, r9"); // compute the element count the insertion needs room for + emitter.instruction("cmp r10, r11"); // does the destination already have room for the insertion? + emitter.instruction("jle __rt_asis_shift_x86"); // yes, start sliding the tail right + emitter.instruction("call __rt_array_grow"); // at least double the destination payload capacity + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // persist the possibly-relocated destination pointer + emitter.instruction("jmp __rt_asis_grow_check_x86"); // keep growing until the insertion fits + + // -- slide the string slots at and after the insertion index to the right -- + emitter.label("__rt_asis_shift_x86"); + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // rsi = requested insertion index + emitter.instruction("mov r10, QWORD PTR [rdi]"); // r10 = destination logical length + emitter.instruction("test rsi, rsi"); // did the caller ask to insert before the first slot? + emitter.instruction("jns __rt_asis_index_low_x86"); // a non-negative index only needs the upper clamp + emitter.instruction("xor esi, esi"); // clamp a negative insertion index to the front + emitter.label("__rt_asis_index_low_x86"); + emitter.instruction("cmp rsi, r10"); // does the insertion index lie past the last slot? + emitter.instruction("jle __rt_asis_index_ready_x86"); // keep an index that still points inside the payload + emitter.instruction("mov rsi, r10"); // clamp an over-large insertion index to an append + emitter.label("__rt_asis_index_ready_x86"); + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // persist the clamped insertion index for the copy loop + emitter.instruction("lea r11, [rdi + 24]"); // r11 = destination string payload base address + emitter.instruction("mov rcx, r10"); // copy the destination length before turning it into a cursor + emitter.instruction("sub rcx, 1"); // rcx = index of the last live destination string slot + emitter.label("__rt_asis_shift_loop_x86"); + emitter.instruction("cmp rcx, rsi"); // have all slots at or after the insertion index moved? + emitter.instruction("jl __rt_asis_copy_x86"); // yes, write the replacement into the opened gap + emitter.instruction("mov rax, rcx"); // copy the source slot index before scaling it + emitter.instruction("shl rax, 4"); // scale it by the 16-byte string slot size + emitter.instruction("mov r8, QWORD PTR [r11 + rax]"); // load the string pointer that has to slide right + emitter.instruction("mov rdi, QWORD PTR [r11 + rax + 8]"); // load the matching string length + emitter.instruction("mov rax, rcx"); // recopy the source slot index for the destination offset + emitter.instruction("add rax, r9"); // compute its slot after the opened gap + emitter.instruction("shl rax, 4"); // scale that slot index by the string slot size + emitter.instruction("mov QWORD PTR [r11 + rax], r8"); // store the string pointer past the opened gap + emitter.instruction("mov QWORD PTR [r11 + rax + 8], rdi"); // store the matching string length past the opened gap + emitter.instruction("sub rcx, 1"); // walk backwards so overlapping slots stay intact + emitter.instruction("jmp __rt_asis_shift_loop_x86"); // continue sliding the tail right + + // -- duplicate the replacement strings into the opened gap -- + emitter.label("__rt_asis_copy_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r9, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("cmp rcx, r9"); // has every replacement string been copied? + emitter.instruction("jge __rt_asis_set_len_x86"); // yes, publish the extended destination length + emitter.instruction("lea r10, [rdx + 24]"); // compute the replacement string payload base address + emitter.instruction("mov rax, rcx"); // copy the replacement copy index before scaling it + emitter.instruction("shl rax, 4"); // scale it by the 16-byte string slot size + emitter.instruction("mov r11, QWORD PTR [r10 + rax]"); // load the borrowed replacement string pointer + emitter.instruction("mov rdx, QWORD PTR [r10 + rax + 8]"); // load the borrowed replacement string length + emitter.instruction("mov rax, r11"); // __rt_str_persist reads its source pointer from rax, not rdi + emitter.instruction("call __rt_str_persist"); // duplicate it so the destination owns its own bytes + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the replacement copy index after the persist call + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the clamped insertion index + emitter.instruction("lea r10, [rdi + 24]"); // compute the destination string payload base address + emitter.instruction("mov r11, rsi"); // copy the insertion index before computing the target slot + emitter.instruction("add r11, rcx"); // compute the destination slot for this replacement element + emitter.instruction("shl r11, 4"); // scale that slot index by the string slot size + emitter.instruction("mov QWORD PTR [r10 + r11], rax"); // store the owned string pointer into the opened gap + emitter.instruction("mov QWORD PTR [r10 + r11 + 8], rdx"); // store the matching owned string length + emitter.instruction("add rcx, 1"); // advance to the next replacement element + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // persist the updated replacement copy index + emitter.instruction("jmp __rt_asis_copy_x86"); // continue copying replacement strings + + // -- publish the extended destination length -- + emitter.label("__rt_asis_set_len_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the destination indexed-array pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the replacement indexed-array pointer + emitter.instruction("mov r9, QWORD PTR [rdx]"); // reload the replacement element count + emitter.instruction("mov r10, QWORD PTR [rdi]"); // reload the previous destination logical length + emitter.instruction("add r10, r9"); // extend it by the inserted element count + emitter.instruction("mov QWORD PTR [rdi], r10"); // persist the extended destination logical length + + emitter.label("__rt_asis_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the possibly-relocated destination pointer + emitter.instruction("add rsp, 64"); // release the insertion bookkeeping slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the caller +} diff --git a/src/codegen_support/runtime/arrays/array_to_hash_reverse.rs b/src/codegen_support/runtime/arrays/array_to_hash_reverse.rs new file mode 100644 index 0000000000..e19e5035b2 --- /dev/null +++ b/src/codegen_support/runtime/arrays/array_to_hash_reverse.rs @@ -0,0 +1,185 @@ +//! Purpose: +//! Emits the `__rt_array_to_hash_reverse` runtime helper backing `array_reverse($a, true)`. +//! Converts an indexed array into an owned hash that keeps the original integer keys but walks +//! them from the last element to the first. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - Mirrors `__rt_array_to_hash` slot for slot; only the iteration direction differs, so the +//! payload handling (string persistence, heap retain, scalar copy) stays identical. +//! - PHP's `preserve_keys` result is key-identical to the source but insertion-order reversed, +//! which is exactly what a hash records and an indexed array cannot represent. +//! - String values are persisted (independent copies) and heap values retained, so the result +//! owns its payloads. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// array_to_hash_reverse: build an owned hash {n-1: e(n-1), …, 0: e0} from an indexed array. +/// Input: x0 = indexed array pointer +/// Output: x0 = new owned hash table with the source integer keys in reverse insertion order +/// +/// Reads the indexed value_type to extract each element: string elements (16-byte slots) +/// are persisted into independent heap copies; heap-backed elements are retained; scalar +/// elements are copied by value. Backs `array_reverse($array, preserve_keys: true)`. +pub fn emit_array_to_hash_reverse(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_to_hash_reverse_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_to_hash_reverse ---"); + emitter.label_global("__rt_array_to_hash_reverse"); + emitter.instruction("sub sp, sp, #80"); // allocate the conversion stack frame + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // set up the new frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the indexed array pointer + emitter.instruction("ldr x9, [x0]"); // load the indexed array length + emitter.instruction("str x9, [sp, #24]"); // save the length + emitter.instruction("ldr x10, [x0, #-8]"); // load the uniform heap-kind header word + emitter.instruction("lsr x10, x10, #8"); // shift the packed value_type into the low bits + emitter.instruction("and x10, x10, #0x7f"); // isolate the indexed-array value_type (also the Mixed tag) + emitter.instruction("str x10, [sp, #32]"); // save the value_type / runtime tag + emitter.instruction("ldr x11, [x0, #16]"); // load the element size (stride) from the header + emitter.instruction("str x11, [sp, #40]"); // save the element stride + emitter.instruction("mov x1, x10"); // value_type for the new hash header + emitter.instruction("cmp x9, #8"); // is the length below the minimum hash capacity? + emitter.instruction("b.ge __rt_array_to_hash_rev_cap_ok"); // use the length as the capacity hint + emitter.instruction("mov x9, #8"); // clamp the capacity hint to a small minimum + emitter.label("__rt_array_to_hash_rev_cap_ok"); + emitter.instruction("mov x0, x9"); // capacity hint for the new hash + emitter.instruction("bl __rt_hash_new"); // allocate the result hash, x0 = result + emitter.instruction("str x0, [sp, #8]"); // save the result hash pointer + emitter.instruction("ldr x9, [sp, #24]"); // reload the length to seed the descending cursor + emitter.instruction("sub x9, x9, #1"); // index i = length - 1, the last source element + emitter.instruction("str x9, [sp, #16]"); // save the descending index + emitter.label("__rt_array_to_hash_rev_loop"); + emitter.instruction("ldr x10, [sp, #16]"); // reload the descending index + emitter.instruction("cmp x10, #0"); // has the index walked past the first element? + emitter.instruction("b.lt __rt_array_to_hash_rev_done"); // all elements converted + emitter.instruction("ldr x11, [sp, #0]"); // reload the indexed array pointer + emitter.instruction("add x11, x11, #24"); // skip the 24-byte indexed-array header + emitter.instruction("ldr x12, [sp, #40]"); // reload the element stride + emitter.instruction("mul x13, x10, x12"); // byte offset of element[i] + emitter.instruction("add x11, x11, x13"); // x11 = address of element[i] + emitter.instruction("ldr x3, [x11]"); // load the element low word + emitter.instruction("str x3, [sp, #48]"); // save the element low word + emitter.instruction("ldr x9, [sp, #32]"); // reload the value_type + emitter.instruction("cmp x9, #1"); // is the element a string? + emitter.instruction("b.eq __rt_array_to_hash_rev_string"); // strings need persistence + emitter.instruction("mov x9, #0"); // non-string elements have no high word + emitter.instruction("str x9, [sp, #56]"); // save a zero high word + emitter.instruction("ldr x9, [sp, #32]"); // reload the value_type + emitter.instruction("cmp x9, #4"); // is the element below the heap-backed tag range? + emitter.instruction("b.lt __rt_array_to_hash_rev_set"); // scalar elements need no retain + emitter.instruction("cmp x9, #7"); // is the element above the heap-backed tag range? + emitter.instruction("b.gt __rt_array_to_hash_rev_set"); // non-heap tags need no retain + emitter.instruction("ldr x0, [sp, #48]"); // load the heap-backed element pointer + emitter.instruction("bl __rt_incref"); // retain the heap-backed element for the result hash + emitter.instruction("b __rt_array_to_hash_rev_set"); // continue to insertion + emitter.label("__rt_array_to_hash_rev_string"); + emitter.instruction("ldr x2, [x11, #8]"); // load the string length from the 16-byte slot + emitter.instruction("ldr x1, [sp, #48]"); // load the string pointer + emitter.instruction("bl __rt_str_persist"); // copy the string into an independent heap block, x1 = new pointer + emitter.instruction("str x1, [sp, #48]"); // save the persisted string pointer + emitter.instruction("str x2, [sp, #56]"); // save the string length + emitter.label("__rt_array_to_hash_rev_set"); + emitter.instruction("ldr x0, [sp, #8]"); // result hash pointer + emitter.instruction("ldr x1, [sp, #16]"); // integer key = the preserved source index i + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer key + emitter.instruction("ldr x3, [sp, #48]"); // value low word + emitter.instruction("ldr x4, [sp, #56]"); // value high word + emitter.instruction("ldr x5, [sp, #32]"); // value runtime tag (= value_type) + emitter.instruction("bl __rt_hash_set"); // insert element[i] at its preserved integer key + emitter.instruction("str x0, [sp, #8]"); // update the result pointer after possible reallocation + emitter.instruction("ldr x10, [sp, #16]"); // reload the descending index + emitter.instruction("sub x10, x10, #1"); // step back to the previous source element + emitter.instruction("str x10, [sp, #16]"); // save the stepped-back index + emitter.instruction("b __rt_array_to_hash_rev_loop"); // continue converting elements + emitter.label("__rt_array_to_hash_rev_done"); + emitter.instruction("ldr x0, [sp, #8]"); // x0 = result hash pointer + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate the stack frame + emitter.instruction("ret"); // return the result hash in x0 +} + +/// x86_64 Linux implementation of `__rt_array_to_hash_reverse`. +/// Input: rdi = indexed array pointer +/// Output: rax = new owned hash with the source integer keys in reverse insertion order +fn emit_array_to_hash_reverse_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_to_hash_reverse ---"); + emitter.label_global("__rt_array_to_hash_reverse"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base + emitter.instruction("sub rsp, 64"); // reserve local slots for the conversion loop state + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the indexed array pointer + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the indexed array length + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the length + emitter.instruction("mov r10, QWORD PTR [rdi - 8]"); // load the uniform heap-kind header word + emitter.instruction("shr r10, 8"); // shift the packed value_type into the low bits + emitter.instruction("and r10, 127"); // isolate the indexed-array value_type (also the Mixed tag) + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the value_type / runtime tag + emitter.instruction("mov r11, QWORD PTR [rdi + 16]"); // load the element size (stride) from the header + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the element stride + emitter.instruction("mov rsi, r10"); // value_type for the new hash header + emitter.instruction("mov rdi, rax"); // capacity hint = length + emitter.instruction("cmp rdi, 8"); // is the length below the minimum hash capacity? + emitter.instruction("jge __rt_array_to_hash_rev_cap_ok"); // use the length as the capacity hint + emitter.instruction("mov rdi, 8"); // clamp the capacity hint to a small minimum + emitter.label("__rt_array_to_hash_rev_cap_ok"); + emitter.instruction("call __rt_hash_new"); // allocate the result hash, rax = result + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the result hash pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the length to seed the descending cursor + emitter.instruction("sub rax, 1"); // index i = length - 1, the last source element + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the descending index + emitter.label("__rt_array_to_hash_rev_loop"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the descending index + emitter.instruction("cmp rax, 0"); // has the index walked past the first element? + emitter.instruction("jl __rt_array_to_hash_rev_done"); // all elements converted + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the indexed array pointer + emitter.instruction("add r10, 24"); // skip the 24-byte indexed-array header + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the element stride + emitter.instruction("imul r11, rax"); // byte offset of element[i] + emitter.instruction("add r10, r11"); // r10 = address of element[i] + emitter.instruction("mov rcx, QWORD PTR [r10]"); // load the element low word + emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the element low word + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the value_type + emitter.instruction("cmp r9, 1"); // is the element a string? + emitter.instruction("je __rt_array_to_hash_rev_string"); // strings need persistence + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // non-string elements have no high word + emitter.instruction("cmp r9, 4"); // is the element below the heap-backed tag range? + emitter.instruction("jl __rt_array_to_hash_rev_set"); // scalar elements need no retain + emitter.instruction("cmp r9, 7"); // is the element above the heap-backed tag range? + emitter.instruction("jg __rt_array_to_hash_rev_set"); // non-heap tags need no retain + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // load the heap-backed element pointer + emitter.instruction("call __rt_incref"); // retain the heap-backed element for the result hash + emitter.instruction("jmp __rt_array_to_hash_rev_set"); // continue to insertion + emitter.label("__rt_array_to_hash_rev_string"); + emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the string length from the 16-byte slot + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // load the string pointer + emitter.instruction("call __rt_str_persist"); // copy the string into an independent heap block, rax = new pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the persisted string pointer + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the string length + emitter.label("__rt_array_to_hash_rev_set"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // result hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // integer key = the preserved source index i + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer key + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // value low word + emitter.instruction("mov r8, QWORD PTR [rbp - 64]"); // value high word + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // value runtime tag (= value_type) + emitter.instruction("call __rt_hash_set"); // insert element[i] at its preserved integer key + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // update the result pointer after possible reallocation + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the descending index + emitter.instruction("sub rax, 1"); // step back to the previous source element + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the stepped-back index + emitter.instruction("jmp __rt_array_to_hash_rev_loop"); // continue converting elements + emitter.label("__rt_array_to_hash_rev_done"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // rax = result hash pointer + emitter.instruction("add rsp, 64"); // release the local slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the result hash in rax +} diff --git a/src/codegen_support/runtime/arrays/hash_new.rs b/src/codegen_support/runtime/arrays/hash_new.rs index 6d2b7da071..463619fa33 100644 --- a/src/codegen_support/runtime/arrays/hash_new.rs +++ b/src/codegen_support/runtime/arrays/hash_new.rs @@ -7,9 +7,14 @@ //! //! Key details: //! - Hash helpers must normalize PHP keys and preserve bucket layout, ownership, and iteration conventions. +//! - `capacity * 64` is validated before the allocation request. `array_fill()` with a non-zero start +//! routes a caller-supplied count straight into this capacity, and an unchecked product wraps to a +//! tiny block whose entry-zeroing loop then runs off the heap. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::data::ARRAY_ALLOC_SIZE_MSG; /// hash_new: create a new hash table on the heap. @@ -19,6 +24,11 @@ use crate::codegen_support::platform::Arch; /// Layout: [count:8][capacity:8][value_type:8][head:8][tail:8][entries...] /// where each entry is 64 bytes: /// [occupied:8][key_ptr:8][key_len:8][value_lo:8][value_hi:8][value_tag:8][prev:8][next:8] +/// +/// # Size validation +/// Negative capacities are clamped to an empty entries region, and any `capacity * 64 + 40` that +/// does not fit in a non-negative machine word terminates the process through +/// `__rt_hash_cap_overflow`. pub fn emit_hash_new(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_hash_new_linux_x86_64(emitter); @@ -29,6 +39,17 @@ pub fn emit_hash_new(emitter: &mut Emitter) { emitter.comment("--- runtime: hash_new ---"); emitter.label_global("__rt_hash_new"); + // -- validate the requested allocation size before touching the heap -- + emitter.instruction("cmp x0, #0"); // is the requested capacity negative? + emitter.instruction("csel x10, x0, xzr, ge"); // clamp negative capacities to an empty entries region + emitter.instruction("mov x9, #64"); // entry size = 64 bytes with per-entry tags and insertion-order links + emitter.instruction("umulh x11, x10, x9"); // x11 = high 64 bits of capacity * 64 + emitter.instruction("cbnz x11, __rt_hash_cap_overflow"); // reject entry regions that do not fit in one machine word + emitter.instruction("mul x10, x10, x9"); // x10 = low 64 bits of capacity * 64 = entries region size + emitter.instruction("adds x10, x10, #40"); // x10 = entries region plus the 40-byte hash header + emitter.instruction("b.hs __rt_hash_cap_overflow"); // reject totals that carried out of the machine word + emitter.instruction("tbnz x10, #63, __rt_hash_cap_overflow"); // reject totals the signed heap-size check would read as negative + // -- set up stack frame, save arguments -- emitter.instruction("sub sp, sp, #32"); // allocate 32 bytes on the stack emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address @@ -36,10 +57,8 @@ pub fn emit_hash_new(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save capacity to stack emitter.instruction("str x1, [sp, #8]"); // save value_type to stack - // -- calculate total size: 40 + capacity * 64 -- - emitter.instruction("mov x9, #64"); // entry size = 64 bytes with per-entry tags and insertion-order links - emitter.instruction("mul x2, x0, x9"); // x2 = capacity * 64 = entries region size - emitter.instruction("add x0, x2, #40"); // x0 = total size (40-byte header + entries) + // -- allocate the validated total size: 40-byte header + capacity * 64 -- + emitter.instruction("mov x0, x10"); // x0 = validated total size (40-byte header + entries) emitter.instruction("bl __rt_heap_alloc"); // allocate memory, x0 = pointer to hash table emitter.instruction("mov x9, #3"); // heap kind 3 = associative array / hash table emitter.instruction("mov x10, #0x8000"); // bit 15 marks heap containers that participate in copy-on-write @@ -74,6 +93,15 @@ pub fn emit_hash_new(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #32"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = hash table pointer + + // -- fatal error: requested hash size cannot be represented -- + emitter.label("__rt_hash_cap_overflow"); + emitter.instruction("mov x0, #2"); // fd = stderr + abi::emit_symbol_address(emitter, "x1", "_arr_cap_err_msg"); + emitter.instruction(&format!("mov x2, #{}", ARRAY_ALLOC_SIZE_MSG.len())); // pass the exact array-size diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 + emitter.syscall(1); } /// x86_64 Linux variant of `emit_hash_new` using the System V AMD64 ABI. @@ -81,6 +109,11 @@ pub fn emit_hash_new(emitter: &mut Emitter) { /// (0=int, 1=str, 2=float, 3=bool, 4=array, 5=assoc, 6=object, 7=mixed, 8=null) /// Output: rax=pointer to hash table /// Layout: [count:8][capacity:8][value_type:8][head:8][tail:8][entries...] — identical to ARM64. +/// +/// # Size validation +/// Mirrors the ARM64 guard: negative capacities are clamped to an empty entries region and any +/// `capacity * 64 + 40` that does not fit in a non-negative machine word terminates the process +/// through `__rt_hash_cap_overflow`. fn emit_hash_new_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: hash_new ---"); @@ -91,9 +124,13 @@ fn emit_hash_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 16"); // reserve local slots for capacity and value_type across the malloc call emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested hash capacity across the allocator call emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested runtime value_type across the allocator call - emitter.instruction("mov rax, rdi"); // copy the capacity into a scratch register before scaling it by the entry size + emitter.instruction("xor rax, rax"); // default the sizing operand to an empty entries region + emitter.instruction("test rdi, rdi"); // is the requested capacity strictly positive? + emitter.instruction("cmovg rax, rdi"); // clamp negative capacities to an empty entries region emitter.instruction("imul rax, 64"); // compute the total bytes needed for the 64-byte hash entry array + emitter.instruction("jo __rt_hash_cap_overflow"); // reject entry regions that do not fit in one machine word emitter.instruction("add rax, 40"); // include the fixed 40-byte hash header in the allocation size + emitter.instruction("jo __rt_hash_cap_overflow"); // reject totals the signed heap-size accounting would read as negative emitter.instruction("call __rt_heap_alloc"); // allocate the hash-table storage through the shared x86_64 heap wrapper emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(0x8003))); // materialize the copy-on-write hash-table heap kind word with the x86_64 heap marker emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as an associative-array heap object in the uniform header @@ -110,8 +147,8 @@ fn emit_hash_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the requested capacity to determine how many entry headers to clear emitter.label("__rt_hash_new_zero"); - emitter.instruction("test r11, r11"); // stop clearing once every entry slot in the requested capacity has been visited - emitter.instruction("je __rt_hash_new_done"); // skip the zeroing loop entirely for a zero-capacity hash table + emitter.instruction("cmp r11, 0"); // stop clearing once every entry slot in the requested capacity has been visited + emitter.instruction("jle __rt_hash_new_done"); // skip the zeroing loop for empty and negative capacities, matching the signed ARM64 guard emitter.instruction("mov QWORD PTR [r10], 0"); // clear the occupied/tombstone marker for the current hash entry slot emitter.instruction("add r10, 64"); // advance the entry cursor to the next hash slot in the entries region emitter.instruction("sub r11, 1"); // decrement the number of remaining hash entry headers to clear @@ -121,4 +158,15 @@ fn emit_hash_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 16"); // release the temporary capacity and value-type spill slots emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the hash-table pointer emitter.instruction("ret"); // return the newly allocated hash-table pointer in rax + + // -- fatal error: requested hash size cannot be represented -- + emitter.label("__rt_hash_cap_overflow"); + emitter.instruction("mov edi, 2"); // fd = stderr for the hash-size fatal error message + abi::emit_symbol_address(emitter, "rsi", "_arr_cap_err_msg"); + emitter.instruction(&format!("mov edx, {}", ARRAY_ALLOC_SIZE_MSG.len())); // pass the exact array-size diagnostic byte count + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // print the fatal hash-size message to stderr + emitter.instruction("mov edi, 1"); // exit code 1 for an unrepresentable hash size + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the hash-size failure } diff --git a/src/codegen_support/runtime/arrays/hash_sort.rs b/src/codegen_support/runtime/arrays/hash_sort.rs new file mode 100644 index 0000000000..675605094e --- /dev/null +++ b/src/codegen_support/runtime/arrays/hash_sort.rs @@ -0,0 +1,490 @@ +//! Purpose: +//! Emits `__rt_hash_ksort`, `__rt_hash_krsort`, `__rt_hash_asort` and `__rt_hash_arsort`, +//! the runtime sorters behind PHP's order-preserving associative-array sorts, plus the +//! shared `__rt_hash_sort_links` engine and its `__rt_hash_sort_triple` operand reader. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::arrays`. +//! - Generated code, through +//! `crate::codegen::lower_inst::builtins::arrays::lower_array_key_sort` and +//! `lower_asort` / `lower_arsort`. +//! +//! Key details: +//! - A hash table's iteration order lives ONLY in the insertion-order doubly-linked list +//! (`header[24]` = head, `header[32]` = tail, entry `+48` = prev, entry `+56` = next). +//! Sorting therefore relinks that chain and never moves a bucket, so open-addressing +//! probe positions, key/value association, and every refcount stay exactly as they were: +//! these helpers acquire, persist and release nothing. +//! - The algorithm is a linked-list insertion sort that scans the already-sorted suffix +//! backwards from its tail. That makes it stable — PHP 8 sorts are stable, and ties are +//! observable for keys such as `'01'` and `' 1'`, which PHP compares equal — and linear +//! on input that is already ordered. +//! - Ordering is delegated to `__rt_php_compare`, so keys and values follow PHP 8's own +//! comparison table (`10 < 'Banana'`, `'0.5' < 2`, …) instead of a byte-wise order. +//! - Callers must split shared tables with `__rt_hash_ensure_unique` first: these helpers +//! mutate the table they are handed. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; +use crate::codegen_support::sentinels::NULL_SENTINEL; + +/// Mode word selecting an ascending key sort (`ksort`). +const MODE_KEY_ASCENDING: i64 = 0; + +/// Mode word selecting a descending key sort (`krsort`). +const MODE_KEY_DESCENDING: i64 = 1; + +/// Mode word selecting an ascending value sort (`asort`). +const MODE_VALUE_ASCENDING: i64 = 2; + +/// Mode word selecting a descending value sort (`arsort`). +const MODE_VALUE_DESCENDING: i64 = 3; + +/// Emits every hash link-order sort helper for the active target. +/// +/// Publishes the four PHP-facing entry points (`__rt_hash_ksort`, `__rt_hash_krsort`, +/// `__rt_hash_asort`, `__rt_hash_arsort`), the shared `__rt_hash_sort_links` engine and +/// the `__rt_hash_sort_triple` operand reader. Every entry point takes the hash-table +/// pointer in the first integer argument register and returns nothing; the table is +/// mutated in place by relinking its insertion-order chain. +pub fn emit_hash_sort(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_hash_sort_entry_points_x86_64(emitter); + emit_hash_sort_links_x86_64(emitter); + emit_hash_sort_triple_x86_64(emitter); + return; + } + emit_hash_sort_entry_points_aarch64(emitter); + emit_hash_sort_links_aarch64(emitter); + emit_hash_sort_triple_aarch64(emitter); +} + +/// Emits the four AArch64 entry stubs that select a mode and enter the shared engine. +/// +/// Each stub loads its mode word into `x1` and tail-branches to `__rt_hash_sort_links`, +/// so the engine's stack frame and return address belong to the original caller. +fn emit_hash_sort_entry_points_aarch64(emitter: &mut Emitter) { + for (label, mode, description) in hash_sort_entry_points() { + emitter.blank(); + emitter.comment(&format!("--- runtime: {} ({}) ---", label, description)); + emitter.label_global(label); + emitter.instruction(&format!("mov x1, #{}", mode)); // select the key/value and ascending/descending sort mode + emitter.instruction("b __rt_hash_sort_links"); // enter the shared insertion-order relinking engine + } +} + +/// Emits the four x86_64 entry stubs that select a mode and enter the shared engine. +/// +/// Each stub loads its mode word into `rsi` and tail-jumps to `__rt_hash_sort_links`, +/// mirroring the AArch64 stubs one-for-one. +fn emit_hash_sort_entry_points_x86_64(emitter: &mut Emitter) { + for (label, mode, description) in hash_sort_entry_points() { + emitter.blank(); + emitter.comment(&format!("--- runtime: {} ({}) ---", label, description)); + emitter.label_global(label); + emitter.instruction(&format!("mov esi, {}", mode)); // select the key/value and ascending/descending sort mode + emitter.instruction("jmp __rt_hash_sort_links"); // enter the shared insertion-order relinking engine + } +} + +/// Returns the PHP-facing hash sort entry points with their mode words and descriptions. +fn hash_sort_entry_points() -> [(&'static str, i64, &'static str); 4] { + [ + ("__rt_hash_ksort", MODE_KEY_ASCENDING, "sort a hash by key ascending"), + ("__rt_hash_krsort", MODE_KEY_DESCENDING, "sort a hash by key descending"), + ("__rt_hash_asort", MODE_VALUE_ASCENDING, "sort a hash by value ascending"), + ("__rt_hash_arsort", MODE_VALUE_DESCENDING, "sort a hash by value descending"), + ] +} + +/// Emits the AArch64 `__rt_hash_sort_links` engine. +/// +/// Input `x0` = hash-table pointer, `x1` = mode word (bit 0 = descending, bit 1 = sort by +/// value). The routine detaches entries from the insertion-order chain one at a time and +/// reinserts each into a growing sorted chain, scanning that chain backwards from its tail +/// so equal operands keep their original relative order. Null pointers, the in-band +/// null-container sentinel, and tables with fewer than two live entries return untouched. +/// +/// Frame (112 bytes): `[sp,#0]` table, `[sp,#8]` entries base, `[sp,#16]` mode, +/// `[sp,#24]` sorted head, `[sp,#32]` sorted tail, `[sp,#40]` current slot, +/// `[sp,#48]` next source slot, `[sp,#56]` backward scan cursor, +/// `[sp,#64..#80]` the current entry's comparison triple, `[sp,#96]` saved `x29`/`x30`. +fn emit_hash_sort_links_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_sort_links ---"); + emitter.label_global("__rt_hash_sort_links"); + + // -- reject containers that carry no sortable insertion-order chain -- + emitter.instruction("cbz x0, __rt_hsort_ret"); // null tables from missed reads have nothing to reorder + abi::emit_load_int_immediate(emitter, "x9", NULL_SENTINEL); + emitter.instruction("cmp x0, x9"); // does the table carry the in-band null-container sentinel? + emitter.instruction("b.eq __rt_hsort_ret"); // sentinel-null tables have no header to relink + emitter.instruction("ldr x9, [x0]"); // x9 = live entry count from the hash header + emitter.instruction("cmp x9, #2"); // does the table hold at least two entries? + emitter.instruction("b.ge __rt_hsort_begin"); // only multi-entry tables can change order + + emitter.label("__rt_hsort_ret"); + emitter.instruction("ret"); // return with the table left exactly as it was + + // -- establish the sort frame and seed an empty destination chain -- + emitter.label("__rt_hsort_begin"); + emitter.instruction("sub sp, sp, #112"); // allocate the link-sort frame + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #96"); // establish the link-sort frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the hash-table pointer for the final header update + emitter.instruction("add x9, x0, #40"); // compute the entries region base past the 40-byte header + emitter.instruction("str x9, [sp, #8]"); // save the entries base used by every slot address computation + emitter.instruction("str x1, [sp, #16]"); // save the key/value and direction mode word + emitter.instruction("mov x9, #-1"); // the destination chain starts empty + emitter.instruction("str x9, [sp, #24]"); // sorted head = none + emitter.instruction("str x9, [sp, #32]"); // sorted tail = none + emitter.instruction("ldr x9, [x0, #24]"); // x9 = current insertion-order head slot + emitter.instruction("str x9, [sp, #40]"); // start consuming the source chain from its head + + // -- outer loop: detach the next source entry and read its comparison triple -- + emitter.label("__rt_hsort_outer"); + emitter.instruction("ldr x9, [sp, #40]"); // reload the slot being placed + emitter.instruction("cmn x9, #1"); // has the source chain been fully consumed? + emitter.instruction("b.eq __rt_hsort_finish"); // publish the sorted chain once no source entry remains + emitter.instruction("ldr x10, [sp, #8]"); // reload the entries region base + emitter.instruction("add x11, x10, x9, lsl #6"); // x11 = address of the 64-byte entry being placed + emitter.instruction("ldr x12, [x11, #56]"); // read the source successor before the entry is relinked + emitter.instruction("str x12, [sp, #48]"); // remember where the source walk resumes + emitter.instruction("mov x0, x11"); // pass the entry address to the operand reader + emitter.instruction("ldr x1, [sp, #16]"); // pass the mode so the reader picks the key or the value + emitter.instruction("bl __rt_hash_sort_triple"); // materialize the entry's PHP comparison triple + emitter.instruction("str x0, [sp, #64]"); // cache the placed entry's runtime tag + emitter.instruction("str x1, [sp, #72]"); // cache the placed entry's low payload word + emitter.instruction("str x2, [sp, #80]"); // cache the placed entry's high payload word + emitter.instruction("ldr x9, [sp, #32]"); // reload the sorted chain's tail + emitter.instruction("str x9, [sp, #56]"); // start the backward scan at that tail + + // -- inner loop: walk the sorted chain backwards to the stable insertion point -- + emitter.label("__rt_hsort_scan"); + emitter.instruction("ldr x9, [sp, #56]"); // reload the backward scan cursor + emitter.instruction("cmn x9, #1"); // has the scan run off the front of the sorted chain? + emitter.instruction("b.eq __rt_hsort_insert"); // the entry belongs at the head of the sorted chain + emitter.instruction("ldr x10, [sp, #8]"); // reload the entries region base + emitter.instruction("add x0, x10, x9, lsl #6"); // x0 = address of the sorted entry under the cursor + emitter.instruction("ldr x1, [sp, #16]"); // pass the mode so the reader picks the key or the value + emitter.instruction("bl __rt_hash_sort_triple"); // materialize the scanned entry's comparison triple + emitter.instruction("ldr x3, [sp, #64]"); // pass the placed entry's tag as the right operand + emitter.instruction("ldr x4, [sp, #72]"); // pass the placed entry's low payload word + emitter.instruction("ldr x5, [sp, #80]"); // pass the placed entry's high payload word + emitter.instruction("bl __rt_php_compare"); // apply PHP 8's ordering table to scanned versus placed + emitter.instruction("ldr x9, [sp, #16]"); // reload the mode word to pick the direction test + emitter.instruction("tbnz x9, #0, __rt_hsort_scan_desc"); // descending sorts invert the stop condition + emitter.instruction("cmp x0, #0"); // does the scanned entry already sort at or before the placed one? + emitter.instruction("b.le __rt_hsort_insert"); // stopping on equality keeps ties in their original order + emitter.instruction("b __rt_hsort_scan_prev"); // otherwise keep walking towards the chain head + + emitter.label("__rt_hsort_scan_desc"); + emitter.instruction("cmp x0, #0"); // does the scanned entry already sort at or before the placed one? + emitter.instruction("b.ge __rt_hsort_insert"); // stopping on equality keeps ties in their original order + + emitter.label("__rt_hsort_scan_prev"); + emitter.instruction("ldr x9, [sp, #56]"); // reload the backward scan cursor + emitter.instruction("ldr x10, [sp, #8]"); // reload the entries region base + emitter.instruction("add x11, x10, x9, lsl #6"); // x11 = address of the scanned entry + emitter.instruction("ldr x12, [x11, #48]"); // follow the sorted chain's predecessor link + emitter.instruction("str x12, [sp, #56]"); // advance the backward scan cursor + emitter.instruction("b __rt_hsort_scan"); // keep scanning for the stable insertion point + + // -- splice the placed entry into the sorted chain -- + emitter.label("__rt_hsort_insert"); + emitter.instruction("ldr x9, [sp, #56]"); // x9 = predecessor slot, or -1 for a head insertion + emitter.instruction("ldr x10, [sp, #8]"); // reload the entries region base + emitter.instruction("ldr x11, [sp, #40]"); // x11 = the slot being placed + emitter.instruction("add x12, x10, x11, lsl #6"); // x12 = address of the entry being placed + emitter.instruction("cmn x9, #1"); // is there a predecessor to splice after? + emitter.instruction("b.ne __rt_hsort_insert_after"); // splice after the located predecessor + + emitter.instruction("mov x13, #-1"); // a head insertion has no predecessor + emitter.instruction("str x13, [x12, #48]"); // placed entry prev = none + emitter.instruction("ldr x13, [sp, #24]"); // reload the current sorted head + emitter.instruction("str x13, [x12, #56]"); // placed entry next = the old sorted head + emitter.instruction("cmn x13, #1"); // was the sorted chain still empty? + emitter.instruction("b.eq __rt_hsort_insert_first"); // the first placed entry is also the sorted tail + emitter.instruction("add x14, x10, x13, lsl #6"); // x14 = address of the old sorted head + emitter.instruction("str x11, [x14, #48]"); // old sorted head prev = the placed entry + emitter.instruction("b __rt_hsort_insert_head"); // publish the new sorted head + + emitter.label("__rt_hsort_insert_first"); + emitter.instruction("str x11, [sp, #32]"); // sorted tail = the first placed entry + + emitter.label("__rt_hsort_insert_head"); + emitter.instruction("str x11, [sp, #24]"); // sorted head = the placed entry + emitter.instruction("b __rt_hsort_advance"); // continue with the next source entry + + emitter.label("__rt_hsort_insert_after"); + emitter.instruction("add x13, x10, x9, lsl #6"); // x13 = address of the predecessor entry + emitter.instruction("ldr x14, [x13, #56]"); // x14 = the predecessor's current successor + emitter.instruction("str x9, [x12, #48]"); // placed entry prev = the predecessor + emitter.instruction("str x14, [x12, #56]"); // placed entry next = the predecessor's old successor + emitter.instruction("str x11, [x13, #56]"); // predecessor next = the placed entry + emitter.instruction("cmn x14, #1"); // was the predecessor the sorted tail? + emitter.instruction("b.eq __rt_hsort_insert_tail"); // then the placed entry becomes the new tail + emitter.instruction("add x15, x10, x14, lsl #6"); // x15 = address of the displaced successor + emitter.instruction("str x11, [x15, #48]"); // displaced successor prev = the placed entry + emitter.instruction("b __rt_hsort_advance"); // continue with the next source entry + + emitter.label("__rt_hsort_insert_tail"); + emitter.instruction("str x11, [sp, #32]"); // sorted tail = the placed entry + + emitter.label("__rt_hsort_advance"); + emitter.instruction("ldr x9, [sp, #48]"); // reload the remembered source successor + emitter.instruction("str x9, [sp, #40]"); // resume the source walk from that entry + emitter.instruction("b __rt_hsort_outer"); // place the next source entry + + // -- publish the sorted chain through the hash header -- + emitter.label("__rt_hsort_finish"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the hash-table pointer + emitter.instruction("ldr x9, [sp, #24]"); // reload the sorted chain head + emitter.instruction("str x9, [x0, #24]"); // header[24]: publish the new iteration-order head + emitter.instruction("ldr x9, [sp, #32]"); // reload the sorted chain tail + emitter.instruction("str x9, [x0, #32]"); // header[32]: publish the new iteration-order tail + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the link-sort frame + emitter.instruction("ret"); // return with the table reordered in place +} + +/// Emits the AArch64 `__rt_hash_sort_triple` operand reader. +/// +/// Input `x0` = hash entry address, `x1` = mode word; output is the `__rt_php_compare` +/// triple `x0` = runtime tag, `x1` = low payload word, `x2` = high payload word. Key mode +/// turns the normalized key encoding (`key_len == -1` marks an integer key) into tag 0 or +/// tag 1; value mode reads the entry payload and peels boxed Mixed cells (tag 7) through a +/// tail branch into `__rt_mixed_unbox`. String payloads stay borrowed from the table. +fn emit_hash_sort_triple_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_sort_triple ---"); + emitter.label_global("__rt_hash_sort_triple"); + + emitter.instruction("tbnz x1, #1, __rt_hsort_triple_value"); // mode bit 1 selects the entry value instead of its key + emitter.instruction("ldr x2, [x0, #16]"); // x2 = stored key length, or -1 for a normalized integer key + emitter.instruction("ldr x1, [x0, #8]"); // x1 = stored key pointer, or the integer key payload + emitter.instruction("cmn x2, #1"); // is this a normalized integer key? + emitter.instruction("b.ne __rt_hsort_triple_key_str"); // string keys keep their pointer and length + emitter.instruction("mov x0, #0"); // runtime tag 0 = int + emitter.instruction("mov x2, #0"); // integer operands carry no high payload word + emitter.instruction("ret"); // return the integer key triple + + emitter.label("__rt_hsort_triple_key_str"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("ret"); // return the borrowed string key triple + + emitter.label("__rt_hsort_triple_value"); + emitter.instruction("ldr x3, [x0, #40]"); // x3 = the entry's per-entry runtime value tag + emitter.instruction("ldr x1, [x0, #24]"); // x1 = the entry's low payload word + emitter.instruction("ldr x2, [x0, #32]"); // x2 = the entry's high payload word + emitter.instruction("cmp x3, #7"); // does the entry hold a boxed Mixed cell? + emitter.instruction("b.eq __rt_hsort_triple_value_boxed"); // boxed cells must be peeled before comparing + emitter.instruction("mov x0, x3"); // unboxed entries already carry a concrete tag + emitter.instruction("ret"); // return the borrowed value triple + + emitter.label("__rt_hsort_triple_value_boxed"); + emitter.instruction("mov x0, x1"); // pass the borrowed Mixed cell to the unboxing helper + emitter.instruction("b __rt_mixed_unbox"); // tail-branch so the peeled triple returns to our caller +} + +/// Emits the x86_64 System V `__rt_hash_sort_links` engine. +/// +/// Input `rdi` = hash-table pointer, `rsi` = mode word (bit 0 = descending, bit 1 = sort +/// by value). Semantics are identical to the AArch64 engine, including the stable backward +/// scan and the untouched-on-empty early exits. +/// +/// Frame (96 bytes below `rbp`): `[rbp-8]` table, `[rbp-16]` entries base, `[rbp-24]` mode, +/// `[rbp-32]` sorted head, `[rbp-40]` sorted tail, `[rbp-48]` current slot, +/// `[rbp-56]` next source slot, `[rbp-64]` backward scan cursor, +/// `[rbp-72..-88]` the current entry's comparison triple. +fn emit_hash_sort_links_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_sort_links ---"); + emitter.label_global("__rt_hash_sort_links"); + + // -- reject containers that carry no sortable insertion-order chain -- + emitter.instruction("test rdi, rdi"); // null tables from missed reads have nothing to reorder + emitter.instruction("jz __rt_hsort_ret"); // return before dereferencing a null table header + abi::emit_load_int_immediate(emitter, "r10", NULL_SENTINEL); + emitter.instruction("cmp rdi, r10"); // does the table carry the in-band null-container sentinel? + emitter.instruction("je __rt_hsort_ret"); // sentinel-null tables have no header to relink + emitter.instruction("mov r10, QWORD PTR [rdi]"); // r10 = live entry count from the hash header + emitter.instruction("cmp r10, 2"); // does the table hold at least two entries? + emitter.instruction("jge __rt_hsort_begin"); // only multi-entry tables can change order + + emitter.label("__rt_hsort_ret"); + emitter.instruction("ret"); // return with the table left exactly as it was + + // -- establish the sort frame and seed an empty destination chain -- + emitter.label("__rt_hsort_begin"); + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the link-sort frame pointer + emitter.instruction("sub rsp, 96"); // allocate the aligned link-sort frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the hash-table pointer for the final header update + emitter.instruction("lea r10, [rdi + 40]"); // compute the entries region base past the 40-byte header + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // save the entries base used by every slot address computation + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the key/value and direction mode word + emitter.instruction("mov QWORD PTR [rbp - 32], -1"); // sorted head = none + emitter.instruction("mov QWORD PTR [rbp - 40], -1"); // sorted tail = none + emitter.instruction("mov r10, QWORD PTR [rdi + 24]"); // r10 = current insertion-order head slot + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // start consuming the source chain from its head + + // -- outer loop: detach the next source entry and read its comparison triple -- + emitter.label("__rt_hsort_outer"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the slot being placed + emitter.instruction("cmp r10, -1"); // has the source chain been fully consumed? + emitter.instruction("je __rt_hsort_finish"); // publish the sorted chain once no source entry remains + emitter.instruction("mov r11, r10"); // copy the slot index before scaling it + emitter.instruction("shl r11, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add r11, QWORD PTR [rbp - 16]"); // r11 = address of the entry being placed + emitter.instruction("mov rax, QWORD PTR [r11 + 56]"); // read the source successor before the entry is relinked + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // remember where the source walk resumes + emitter.instruction("mov rdi, r11"); // pass the entry address to the operand reader + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the mode so the reader picks the key or the value + emitter.instruction("call __rt_hash_sort_triple"); // materialize the entry's PHP comparison triple + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // cache the placed entry's runtime tag + emitter.instruction("mov QWORD PTR [rbp - 80], rdi"); // cache the placed entry's low payload word + emitter.instruction("mov QWORD PTR [rbp - 88], rdx"); // cache the placed entry's high payload word + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the sorted chain's tail + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // start the backward scan at that tail + + // -- inner loop: walk the sorted chain backwards to the stable insertion point -- + emitter.label("__rt_hsort_scan"); + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // reload the backward scan cursor + emitter.instruction("cmp r10, -1"); // has the scan run off the front of the sorted chain? + emitter.instruction("je __rt_hsort_insert"); // the entry belongs at the head of the sorted chain + emitter.instruction("mov r11, r10"); // copy the cursor slot index before scaling it + emitter.instruction("shl r11, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add r11, QWORD PTR [rbp - 16]"); // r11 = address of the sorted entry under the cursor + emitter.instruction("mov rdi, r11"); // pass the entry address to the operand reader + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the mode so the reader picks the key or the value + emitter.instruction("call __rt_hash_sort_triple"); // materialize the scanned entry's comparison triple + emitter.instruction("mov rsi, rdi"); // move the scanned low payload word into the left-operand slot + emitter.instruction("mov rdi, rax"); // move the scanned runtime tag into the left-operand slot + emitter.instruction("mov rcx, QWORD PTR [rbp - 72]"); // pass the placed entry's tag as the right operand + emitter.instruction("mov r8, QWORD PTR [rbp - 80]"); // pass the placed entry's low payload word + emitter.instruction("mov r9, QWORD PTR [rbp - 88]"); // pass the placed entry's high payload word + emitter.instruction("call __rt_php_compare"); // apply PHP 8's ordering table to scanned versus placed + emitter.instruction("test QWORD PTR [rbp - 24], 1"); // reload the mode word to pick the direction test + emitter.instruction("jnz __rt_hsort_scan_desc"); // descending sorts invert the stop condition + emitter.instruction("cmp rax, 0"); // does the scanned entry already sort at or before the placed one? + emitter.instruction("jle __rt_hsort_insert"); // stopping on equality keeps ties in their original order + emitter.instruction("jmp __rt_hsort_scan_prev"); // otherwise keep walking towards the chain head + + emitter.label("__rt_hsort_scan_desc"); + emitter.instruction("cmp rax, 0"); // does the scanned entry already sort at or before the placed one? + emitter.instruction("jge __rt_hsort_insert"); // stopping on equality keeps ties in their original order + + emitter.label("__rt_hsort_scan_prev"); + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // reload the backward scan cursor + emitter.instruction("shl r10, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add r10, QWORD PTR [rbp - 16]"); // r10 = address of the scanned entry + emitter.instruction("mov r11, QWORD PTR [r10 + 48]"); // follow the sorted chain's predecessor link + emitter.instruction("mov QWORD PTR [rbp - 64], r11"); // advance the backward scan cursor + emitter.instruction("jmp __rt_hsort_scan"); // keep scanning for the stable insertion point + + // -- splice the placed entry into the sorted chain -- + emitter.label("__rt_hsort_insert"); + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // r10 = predecessor slot, or -1 for a head insertion + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // r11 = the slot being placed + emitter.instruction("mov rax, r11"); // copy the placed slot index before scaling it + emitter.instruction("shl rax, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add rax, QWORD PTR [rbp - 16]"); // rax = address of the entry being placed + emitter.instruction("cmp r10, -1"); // is there a predecessor to splice after? + emitter.instruction("jne __rt_hsort_insert_after"); // splice after the located predecessor + + emitter.instruction("mov QWORD PTR [rax + 48], -1"); // placed entry prev = none + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the current sorted head + emitter.instruction("mov QWORD PTR [rax + 56], rcx"); // placed entry next = the old sorted head + emitter.instruction("cmp rcx, -1"); // was the sorted chain still empty? + emitter.instruction("je __rt_hsort_insert_first"); // the first placed entry is also the sorted tail + emitter.instruction("shl rcx, 6"); // convert the old head slot index into an entry offset + emitter.instruction("add rcx, QWORD PTR [rbp - 16]"); // rcx = address of the old sorted head + emitter.instruction("mov QWORD PTR [rcx + 48], r11"); // old sorted head prev = the placed entry + emitter.instruction("jmp __rt_hsort_insert_head"); // publish the new sorted head + + emitter.label("__rt_hsort_insert_first"); + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // sorted tail = the first placed entry + + emitter.label("__rt_hsort_insert_head"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // sorted head = the placed entry + emitter.instruction("jmp __rt_hsort_advance"); // continue with the next source entry + + emitter.label("__rt_hsort_insert_after"); + emitter.instruction("mov rcx, r10"); // copy the predecessor slot index before scaling it + emitter.instruction("shl rcx, 6"); // convert the slot index into a 64-byte entry offset + emitter.instruction("add rcx, QWORD PTR [rbp - 16]"); // rcx = address of the predecessor entry + emitter.instruction("mov rdx, QWORD PTR [rcx + 56]"); // rdx = the predecessor's current successor + emitter.instruction("mov QWORD PTR [rax + 48], r10"); // placed entry prev = the predecessor + emitter.instruction("mov QWORD PTR [rax + 56], rdx"); // placed entry next = the predecessor's old successor + emitter.instruction("mov QWORD PTR [rcx + 56], r11"); // predecessor next = the placed entry + emitter.instruction("cmp rdx, -1"); // was the predecessor the sorted tail? + emitter.instruction("je __rt_hsort_insert_tail"); // then the placed entry becomes the new tail + emitter.instruction("shl rdx, 6"); // convert the displaced successor index into an entry offset + emitter.instruction("add rdx, QWORD PTR [rbp - 16]"); // rdx = address of the displaced successor + emitter.instruction("mov QWORD PTR [rdx + 48], r11"); // displaced successor prev = the placed entry + emitter.instruction("jmp __rt_hsort_advance"); // continue with the next source entry + + emitter.label("__rt_hsort_insert_tail"); + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // sorted tail = the placed entry + + emitter.label("__rt_hsort_advance"); + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the remembered source successor + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // resume the source walk from that entry + emitter.instruction("jmp __rt_hsort_outer"); // place the next source entry + + // -- publish the sorted chain through the hash header -- + emitter.label("__rt_hsort_finish"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the hash-table pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the sorted chain head + emitter.instruction("mov QWORD PTR [rdi + 24], r10"); // header[24]: publish the new iteration-order head + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the sorted chain tail + emitter.instruction("mov QWORD PTR [rdi + 32], r10"); // header[32]: publish the new iteration-order tail + emitter.instruction("mov rsp, rbp"); // release the link-sort frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with the table reordered in place +} + +/// Emits the x86_64 System V `__rt_hash_sort_triple` operand reader. +/// +/// Input `rdi` = hash entry address, `rsi` = mode word; output `rax` = runtime tag, +/// `rdi` = low payload word, `rdx` = high payload word — the same register triple +/// `__rt_mixed_unbox` returns, so boxed values are peeled with a tail jump. String +/// payloads stay borrowed from the table. +fn emit_hash_sort_triple_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_sort_triple ---"); + emitter.label_global("__rt_hash_sort_triple"); + + emitter.instruction("test rsi, 2"); // mode bit 1 selects the entry value instead of its key + emitter.instruction("jnz __rt_hsort_triple_value"); // value sorts read the payload instead of the key + emitter.instruction("mov rdx, QWORD PTR [rdi + 16]"); // rdx = stored key length, or -1 for a normalized integer key + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // r10 = stored key pointer, or the integer key payload + emitter.instruction("cmp rdx, -1"); // is this a normalized integer key? + emitter.instruction("jne __rt_hsort_triple_key_str"); // string keys keep their pointer and length + emitter.instruction("xor eax, eax"); // runtime tag 0 = int + emitter.instruction("mov rdi, r10"); // publish the integer key payload as the low word + emitter.instruction("xor edx, edx"); // integer operands carry no high payload word + emitter.instruction("ret"); // return the integer key triple + + emitter.label("__rt_hsort_triple_key_str"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("mov rdi, r10"); // publish the borrowed key pointer as the low word + emitter.instruction("ret"); // return the borrowed string key triple + + emitter.label("__rt_hsort_triple_value"); + emitter.instruction("mov r11, QWORD PTR [rdi + 40]"); // r11 = the entry's per-entry runtime value tag + emitter.instruction("mov r10, QWORD PTR [rdi + 24]"); // r10 = the entry's low payload word + emitter.instruction("mov rdx, QWORD PTR [rdi + 32]"); // rdx = the entry's high payload word + emitter.instruction("cmp r11, 7"); // does the entry hold a boxed Mixed cell? + emitter.instruction("je __rt_hsort_triple_value_boxed"); // boxed cells must be peeled before comparing + emitter.instruction("mov rax, r11"); // unboxed entries already carry a concrete tag + emitter.instruction("mov rdi, r10"); // publish the borrowed payload as the low word + emitter.instruction("ret"); // return the borrowed value triple + + emitter.label("__rt_hsort_triple_value_boxed"); + emitter.instruction("mov rax, r10"); // pass the borrowed Mixed cell to the unboxing helper + emitter.instruction("jmp __rt_mixed_unbox"); // tail-jump so the peeled triple returns to our caller +} diff --git a/src/codegen_support/runtime/arrays/heap_alloc.rs b/src/codegen_support/runtime/arrays/heap_alloc.rs index ea66217cd0..d4d74e77f2 100644 --- a/src/codegen_support/runtime/arrays/heap_alloc.rs +++ b/src/codegen_support/runtime/arrays/heap_alloc.rs @@ -237,8 +237,8 @@ pub fn emit_heap_alloc(emitter: &mut Emitter) { emitter.instruction("add x12, x12, #16"); // x12 = offset + requested + header (16 bytes) crate::codegen_support::abi::emit_symbol_address(emitter, "x13", "_heap_max"); emitter.instruction("ldr x13, [x13]"); // x13 = heap max size in bytes - emitter.instruction("cmp x12, x13"); // does the allocation fit? - emitter.instruction("b.gt __rt_heap_exhausted"); // no — fatal error + emitter.instruction("cmp x12, x13"); // does the allocation fit (unsigned, so a wrapped size stays above the limit)? + emitter.instruction("b.hi __rt_heap_exhausted"); // no — fatal error // -- compute base address of heap buffer -- crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_heap_buf"); diff --git a/src/codegen_support/runtime/arrays/int_pow_checked.rs b/src/codegen_support/runtime/arrays/int_pow_checked.rs new file mode 100644 index 0000000000..b25d4a971f --- /dev/null +++ b/src/codegen_support/runtime/arrays/int_pow_checked.rs @@ -0,0 +1,296 @@ +//! Purpose: +//! Emits `__rt_int_pow_checked`, the runtime implementation of PHP's `int ** int` +//! (`zend_pow_function_base`): an integer result whenever the exponent is non-negative +//! and the value fits `i64`, a double otherwise. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::arrays`. +//! - Generated code through `Op::ICheckedPow` +//! (`crate::codegen::lower_inst::arithmetic::lower_int_checked_binop`). +//! +//! Key details: +//! - PHP does NOT compute `int ** int` as `pow((double) base, (double) exp)`. It runs a +//! square-and-multiply loop over `i64` and only bails out to `double` at the exact +//! multiplication that overflows, combining the exact accumulator with `pow()` of the +//! remaining factor. The two differ in the last ULP for most overflowing inputs, so the +//! loop is reproduced verbatim — it is the same algorithm the compile-time folder in +//! `crate::optimize::fold::ops::try_fold_int_pow` implements, and the two must agree. +//! - A negative exponent is plain `pow((double) base, (double) exp)`, always a double. +//! - `exp == 0` is `int(1)` (even for base `0`), and `base == 0` with a positive exponent +//! is `int(0)`; both are answered before the loop, exactly like php-src. +//! - Input/output follow the checked-binop helper contract shared with +//! `__rt_int_{add,sub,mul}_checked`: two raw I64 operands in, one boxed Mixed cell out. +//! - The entry point is fully self-contained (no cross-function local-label branches) to +//! survive macOS `.subsections_via_symbols` dead-stripping. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the checked integer exponentiation helper for both AArch64 and x86_64. +/// +/// Input (AArch64): x0 = base I64, x1 = exponent I64 +/// Input (x86_64): rdi = base I64, rsi = exponent I64 +/// Output: boxed Mixed pointer in the integer result register (x0 / rax), tagged `0` +/// (integer) when PHP keeps an int and `2` (double) when PHP promotes. +pub fn emit_int_pow_checked(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_int_pow_checked_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: int_pow_checked ---"); + emitter.label_global("__rt_int_pow_checked"); + + // -- frame slots: l1=[sp,#0] accumulator, l2=[sp,#8] factor, i=[sp,#16] exponent, dval=[sp,#24] -- + emitter.instruction("sub sp, sp, #64"); // allocate the loop state and saved FP/LR area + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + + emitter.instruction("cmp x1, #0"); // is the exponent negative? + emitter.instruction("b.lt __rt_int_pow_checked_neg"); // a negative exponent is always a double result + emitter.instruction("cbz x1, __rt_int_pow_checked_one"); // anything to the power of zero is int(1) + emitter.instruction("cbz x0, __rt_int_pow_checked_zero"); // zero to a positive power is int(0) + + emitter.instruction("mov x2, #1"); // seed the exact accumulator + emitter.instruction("str x2, [sp, #0]"); // l1 = 1 + emitter.instruction("str x0, [sp, #8]"); // l2 = base + emitter.instruction("str x1, [sp, #16]"); // i = exponent + + emitter.label("__rt_int_pow_checked_loop"); + emitter.instruction("ldr x3, [sp, #16]"); // reload the remaining exponent + emitter.instruction("cmp x3, #1"); // php-src loops while the exponent is at least 1 + emitter.instruction("b.lt __rt_int_pow_checked_int"); // exhausted: return the exact accumulator + emitter.instruction("tst x3, #1"); // is the remaining exponent odd? + emitter.instruction("b.eq __rt_int_pow_checked_even"); // even exponents square the factor + + // -- odd step: i -= 1; l1 *= l2 with signed-overflow detection -- + emitter.instruction("sub x3, x3, #1"); // consume one factor from the exponent + emitter.instruction("str x3, [sp, #16]"); // publish the decremented exponent + emitter.instruction("ldr x4, [sp, #0]"); // reload the accumulator + emitter.instruction("ldr x5, [sp, #8]"); // reload the current factor + emitter.instruction("mul x6, x4, x5"); // low half of the signed product + emitter.instruction("smulh x7, x4, x5"); // high half needed for overflow detection + emitter.instruction("cmp x7, x6, asr #63"); // high half must equal the sign extension of the low half + emitter.instruction("b.ne __rt_int_pow_checked_odd_of"); // overflow: promote exactly the way php-src does + emitter.instruction("str x6, [sp, #0]"); // l1 = l1 * l2 + emitter.instruction("b __rt_int_pow_checked_next"); // continue the square-and-multiply loop + + // -- even step: i /= 2; l2 *= l2 with signed-overflow detection -- + emitter.label("__rt_int_pow_checked_even"); + emitter.instruction("lsr x3, x3, #1"); // halve the remaining exponent (it is non-negative) + emitter.instruction("str x3, [sp, #16]"); // publish the halved exponent + emitter.instruction("ldr x5, [sp, #8]"); // reload the current factor + emitter.instruction("mul x6, x5, x5"); // low half of the squared factor + emitter.instruction("smulh x7, x5, x5"); // high half needed for overflow detection + emitter.instruction("cmp x7, x6, asr #63"); // high half must equal the sign extension of the low half + emitter.instruction("b.ne __rt_int_pow_checked_even_of"); // overflow: promote exactly the way php-src does + emitter.instruction("str x6, [sp, #8]"); // l2 = l2 * l2 + emitter.instruction("b __rt_int_pow_checked_next"); // continue the square-and-multiply loop + + emitter.label("__rt_int_pow_checked_next"); + emitter.instruction("ldr x3, [sp, #16]"); // reload the remaining exponent + emitter.instruction("cbz x3, __rt_int_pow_checked_int"); // exponent consumed: the accumulator is exact + emitter.instruction("b __rt_int_pow_checked_loop"); // otherwise keep going + + // -- odd overflow: result = ((double) l1 * (double) l2) * pow((double) l2, (double) i) -- + emitter.label("__rt_int_pow_checked_odd_of"); + emitter.instruction("scvtf d0, x4"); // (double) accumulator before the overflowing multiply + emitter.instruction("scvtf d1, x5"); // (double) factor + emitter.instruction("fmul d0, d0, d1"); // php-src's ZEND_SIGNED_MULTIPLY_LONG dval + emitter.instruction("str d0, [sp, #24]"); // save dval across the libc pow call + emitter.instruction("scvtf d0, x5"); // pow base = (double) factor + emitter.instruction("ldr x3, [sp, #16]"); // remaining exponent after the decrement + emitter.instruction("scvtf d1, x3"); // pow exponent = (double) remaining + emitter.bl_c("pow"); // pow(l2, i) + emitter.instruction("ldr d1, [sp, #24]"); // reload dval + emitter.instruction("fmul d0, d0, d1"); // dval * pow(l2, i) + emitter.instruction("b __rt_int_pow_checked_box_double"); // box the promoted double + + // -- even overflow: result = (double) l1 * pow((double) l2 * (double) l2, (double) i) -- + emitter.label("__rt_int_pow_checked_even_of"); + emitter.instruction("scvtf d0, x5"); // (double) factor before the overflowing square + emitter.instruction("fmul d0, d0, d0"); // php-src's ZEND_SIGNED_MULTIPLY_LONG dval + emitter.instruction("ldr x3, [sp, #16]"); // remaining exponent after the halving + emitter.instruction("scvtf d1, x3"); // pow exponent = (double) remaining + emitter.bl_c("pow"); // pow(dval, i) + emitter.instruction("ldr x4, [sp, #0]"); // reload the exact accumulator + emitter.instruction("scvtf d1, x4"); // (double) accumulator + emitter.instruction("fmul d0, d0, d1"); // l1 * pow(dval, i) + emitter.instruction("b __rt_int_pow_checked_box_double"); // box the promoted double + + // -- negative exponent: pow((double) base, (double) exp) -- + emitter.label("__rt_int_pow_checked_neg"); + emitter.instruction("scvtf d0, x0"); // (double) base + emitter.instruction("scvtf d1, x1"); // (double) exponent + emitter.bl_c("pow"); // pow(base, exp) + emitter.instruction("b __rt_int_pow_checked_box_double"); // box the double result + + emitter.label("__rt_int_pow_checked_one"); + emitter.instruction("mov x1, #1"); // PHP answers int(1) for a zero exponent + emitter.instruction("b __rt_int_pow_checked_box_int"); // box the integer result + + emitter.label("__rt_int_pow_checked_zero"); + emitter.instruction("mov x1, #0"); // PHP answers int(0) for zero to a positive power + emitter.instruction("b __rt_int_pow_checked_box_int"); // box the integer result + + emitter.label("__rt_int_pow_checked_int"); + emitter.instruction("ldr x1, [sp, #0]"); // the exact accumulator is the integer result + + emitter.label("__rt_int_pow_checked_box_int"); + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the integer result into a Mixed cell + emitter.instruction("b __rt_int_pow_checked_done"); // restore the helper frame and return + + emitter.label("__rt_int_pow_checked_box_double"); + emitter.instruction("fmov x1, d0"); // move the double bits into the Mixed helper payload register + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the promoted double into a Mixed cell + + emitter.label("__rt_int_pow_checked_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the helper stack frame + emitter.instruction("ret"); // return to generated code with the boxed Mixed result in x0 +} + +/// Emits the Linux x86_64 variant of `__rt_int_pow_checked`. +/// +/// Mirrors the AArch64 square-and-multiply loop with SysV registers: `rdi` = base, +/// `rsi` = exponent, boxed Mixed pointer returned in `rax`. Overflow is detected with the +/// one-operand `imul` overflow flag, which is the x86 equivalent of the AArch64 +/// `smulh`/`asr #63` comparison. +fn emit_int_pow_checked_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: int_pow_checked ---"); + emitter.label_global("__rt_int_pow_checked"); + + // -- frame slots: l1=[rbp-8] accumulator, l2=[rbp-16] factor, i=[rbp-24] exponent, dval=[rbp-32] -- + emitter.instruction("push rbp"); // save the caller frame pointer before nested runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // allocate aligned slots for the loop state + + emitter.instruction("test rsi, rsi"); // inspect the exponent's sign + emitter.instruction("js __rt_int_pow_checked_neg_x"); // a negative exponent is always a double result + emitter.instruction("jz __rt_int_pow_checked_one_x"); // anything to the power of zero is int(1) + emitter.instruction("test rdi, rdi"); // is the base zero? + emitter.instruction("jz __rt_int_pow_checked_zero_x"); // zero to a positive power is int(0) + + emitter.instruction("mov QWORD PTR [rbp - 8], 1"); // l1 = 1 + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // l2 = base + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // i = exponent + + emitter.label("__rt_int_pow_checked_loop_x"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // reload the remaining exponent + emitter.instruction("cmp rcx, 1"); // php-src loops while the exponent is at least 1 + emitter.instruction("jl __rt_int_pow_checked_int_x"); // exhausted: return the exact accumulator + emitter.instruction("test rcx, 1"); // is the remaining exponent odd? + emitter.instruction("jz __rt_int_pow_checked_even_x"); // even exponents square the factor + + emitter.instruction("dec rcx"); // consume one factor from the exponent + emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // publish the decremented exponent + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the accumulator into the multiply operand + emitter.instruction("imul QWORD PTR [rbp - 16]"); // rdx:rax = l1 * l2 with the overflow flag set + emitter.instruction("jo __rt_int_pow_checked_odd_of_x"); // overflow: promote exactly the way php-src does + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // l1 = l1 * l2 + emitter.instruction("jmp __rt_int_pow_checked_next_x"); // continue the square-and-multiply loop + + emitter.label("__rt_int_pow_checked_even_x"); + emitter.instruction("shr rcx, 1"); // halve the remaining exponent (it is non-negative) + emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // publish the halved exponent + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the factor into the multiply operand + emitter.instruction("imul QWORD PTR [rbp - 16]"); // rdx:rax = l2 * l2 with the overflow flag set + emitter.instruction("jo __rt_int_pow_checked_even_of_x"); // overflow: promote exactly the way php-src does + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // l2 = l2 * l2 + emitter.instruction("jmp __rt_int_pow_checked_next_x"); // continue the square-and-multiply loop + + emitter.label("__rt_int_pow_checked_next_x"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // has the exponent been fully consumed? + emitter.instruction("je __rt_int_pow_checked_int_x"); // the accumulator is exact + emitter.instruction("jmp __rt_int_pow_checked_loop_x"); // otherwise keep going + + emitter.label("__rt_int_pow_checked_odd_of_x"); + emitter.instruction("cvtsi2sd xmm0, QWORD PTR [rbp - 8]"); // (double) accumulator before the overflowing multiply + emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rbp - 16]"); // (double) factor + emitter.instruction("mulsd xmm0, xmm1"); // php-src's ZEND_SIGNED_MULTIPLY_LONG dval + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save dval across the libc pow call + emitter.instruction("cvtsi2sd xmm0, QWORD PTR [rbp - 16]"); // pow base = (double) factor + emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rbp - 24]"); // pow exponent = (double) remaining + emitter.bl_c("pow"); // pow(l2, i) + emitter.instruction("mulsd xmm0, QWORD PTR [rbp - 32]"); // dval * pow(l2, i) + emitter.instruction("jmp __rt_int_pow_checked_box_double_x"); // box the promoted double + + emitter.label("__rt_int_pow_checked_even_of_x"); + emitter.instruction("cvtsi2sd xmm0, QWORD PTR [rbp - 16]"); // (double) factor before the overflowing square + emitter.instruction("mulsd xmm0, xmm0"); // php-src's ZEND_SIGNED_MULTIPLY_LONG dval + emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rbp - 24]"); // pow exponent = (double) remaining + emitter.bl_c("pow"); // pow(dval, i) + emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rbp - 8]"); // (double) exact accumulator + emitter.instruction("mulsd xmm0, xmm1"); // l1 * pow(dval, i) + emitter.instruction("jmp __rt_int_pow_checked_box_double_x"); // box the promoted double + + emitter.label("__rt_int_pow_checked_neg_x"); + emitter.instruction("cvtsi2sd xmm0, rdi"); // (double) base + emitter.instruction("cvtsi2sd xmm1, rsi"); // (double) exponent + emitter.bl_c("pow"); // pow(base, exp) + emitter.instruction("jmp __rt_int_pow_checked_box_double_x"); // box the double result + + emitter.label("__rt_int_pow_checked_one_x"); + emitter.instruction("mov rdi, 1"); // PHP answers int(1) for a zero exponent + emitter.instruction("jmp __rt_int_pow_checked_box_int_x"); // box the integer result + + emitter.label("__rt_int_pow_checked_zero_x"); + emitter.instruction("xor edi, edi"); // PHP answers int(0) for zero to a positive power + emitter.instruction("jmp __rt_int_pow_checked_box_int_x"); // box the integer result + + emitter.label("__rt_int_pow_checked_int_x"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // the exact accumulator is the integer result + + emitter.label("__rt_int_pow_checked_box_int_x"); + emitter.instruction("xor rsi, rsi"); // integer payloads do not use a high word + emitter.instruction("mov rax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the integer result into a Mixed cell + emitter.instruction("jmp __rt_int_pow_checked_done_x"); // restore the helper frame and return + + emitter.label("__rt_int_pow_checked_box_double_x"); + emitter.instruction("movq rdi, xmm0"); // move the double bits into the Mixed helper payload register + emitter.instruction("xor rsi, rsi"); // double payloads do not use a high word + emitter.instruction("mov rax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the promoted double into a Mixed cell + + emitter.label("__rt_int_pow_checked_done_x"); + emitter.instruction("add rsp, 64"); // release the helper stack frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to generated code with the boxed Mixed result in rax +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies both targets emit the php-src square-and-multiply structure: the odd and + /// even overflow bail-outs, the negative-exponent `pow` path, and both boxing tags. + #[test] + fn test_emit_int_pow_checked_covers_php_algorithm() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_int_pow_checked(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_int_pow_checked:\n"), "missing entry point for {:?}", arch); + for fragment in [ + "__rt_int_pow_checked_odd_of", + "__rt_int_pow_checked_even_of", + "__rt_int_pow_checked_neg", + "__rt_int_pow_checked_box_int", + "__rt_int_pow_checked_box_double", + ] { + assert!(asm.contains(fragment), "missing {} for {:?}", fragment, arch); + } + assert!(asm.contains("__rt_mixed_from_value"), "missing boxing call for {:?}", arch); + } + } +} diff --git a/src/codegen_support/runtime/arrays/ksort.rs b/src/codegen_support/runtime/arrays/ksort.rs deleted file mode 100644 index d27e53ec93..0000000000 --- a/src/codegen_support/runtime/arrays/ksort.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Emits the `__rt_ksort`, `__rt_krsort` runtime helper assembly for ksort. -//! Keeps PHP array/hash storage, heap ownership, and target-specific ABI variants in one focused emitter. -//! -//! Called from: -//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. -//! -//! Key details: -//! - Sort helpers mutate array payload order in place and must preserve PHP comparison behavior for supported value kinds. - -use crate::codegen_support::emit::Emitter; - -/// Emits `__rt_ksort` and `__rt_krsort` runtime helpers into the assembly buffer. -/// -/// `__rt_ksort` sorts a PHP array by keys in ascending order. -/// `__rt_krsort` sorts a PHP array by keys in descending order. -/// For indexed (integer-keyed) arrays, elements are already ordered by numeric -/// index, so both functions are no-ops that return immediately without modifying -/// the array payload. -pub fn emit_ksort(emitter: &mut Emitter) { - emitter.blank(); - emitter.comment("--- runtime: ksort (sort by keys ascending, no-op for indexed) ---"); - emitter.label_global("__rt_ksort"); - - // -- indexed arrays are already in key order (0, 1, 2, ...) -- - emitter.instruction("ret"); // return immediately, array unchanged - - emitter.blank(); - emitter.comment("--- runtime: krsort (sort by keys descending, no-op for indexed) ---"); - emitter.label_global("__rt_krsort"); - - // -- indexed arrays are already in key order, reverse would need reindexing -- - emitter.instruction("ret"); // return immediately, array unchanged -} diff --git a/src/codegen_support/runtime/arrays/min_max_container.rs b/src/codegen_support/runtime/arrays/min_max_container.rs new file mode 100644 index 0000000000..9810aeec34 --- /dev/null +++ b/src/codegen_support/runtime/arrays/min_max_container.rs @@ -0,0 +1,516 @@ +//! Purpose: +//! Emits `__rt_min_max_mixed`, `__rt_min_max_str` and `__rt_min_max_hash`, the runtime +//! reductions behind PHP's single-array `min()` / `max()` form for the container shapes +//! whose elements cannot be compared as raw 8-byte scalar words: indexed arrays of boxed +//! `Mixed` cells, indexed arrays of strings, and hash-backed associative arrays. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::arrays`. +//! - Generated code, through +//! `crate::codegen::lower_inst::builtins::math::min_max_array`. +//! +//! Key details: +//! - All three helpers implement PHP's own reduction: the first element seeds the result +//! and a later element only replaces it on a strict win, so ties keep the *earlier* +//! element and the winner keeps its original runtime tag. +//! - Comparison is delegated to `__rt_php_compare`, so every container shape agrees on +//! PHP 8's ordering table. +//! - The result is the unboxed `(tag, lo, hi)` triple of the winning element: AArch64 +//! `x0`/`x1`/`x2`, x86_64 `rax`/`rdi`/`rsi` (the exact input registers of +//! `__rt_mixed_from_value`, so the caller can box it with one call). Tag `-1` reports +//! an empty or null container, which the caller turns into PHP's `ValueError`. +//! - String payloads stay **borrowed** from the container: nothing is persisted, retained +//! or released here. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Byte offset of the first payload slot inside an indexed-array allocation. +const ARRAY_DATA_OFFSET: i64 = 24; + +/// Emits `__rt_min_max_mixed`: reduces an indexed array of boxed `Mixed` cells. +/// +/// Input: AArch64 `x0` = array pointer, `x1` = 1 for `max()` and 0 for `min()`; +/// x86_64 `rdi` = array pointer, `rsi` = the same flag. Output is the winning +/// element's unboxed triple, or tag `-1` when the array holds no element. +/// Element cells stay borrowed. +pub fn emit_min_max_mixed(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_min_max_mixed_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: min_max_mixed ---"); + emitter.label_global("__rt_min_max_mixed"); + + // Frame (96 bytes): [0]=array [8]=length [16]=cursor [24]=want_max + // [32..48]=best tag/lo/hi [56..72]=candidate tag/lo/hi [80]=x29/x30 + emitter.instruction("sub sp, sp, #96"); // allocate the reduction frame + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the reduction frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the source indexed-array pointer + emitter.instruction("str x1, [sp, #24]"); // save the min/max direction flag + emitter.instruction("mov x9, #-1"); // tag -1 reports "the container yielded no element" + emitter.instruction("str x9, [sp, #32]"); // seed the running result with the empty sentinel + emitter.instruction("str xzr, [sp, #40]"); // clear the running low payload word + emitter.instruction("str xzr, [sp, #48]"); // clear the running high payload word + emitter.instruction("cbz x0, __rt_mmm_done"); // a null container behaves like an empty array + emitter.instruction("ldr x9, [x0]"); // load the array's logical element count from its header + emitter.instruction("str x9, [sp, #8]"); // preserve the element count across the helper calls + emitter.instruction("cbz x9, __rt_mmm_done"); // an empty array yields the sentinel tag + + // -- seed the reduction with the first element -- + emitter.instruction(&format!("ldr x0, [x0, #{}]", ARRAY_DATA_OFFSET)); // load the borrowed Mixed cell of element zero + emitter.instruction("bl __rt_mixed_unbox"); // peel the cell into a concrete tag/lo/hi triple + emitter.instruction("str x0, [sp, #32]"); // seed the running runtime tag + emitter.instruction("str x1, [sp, #40]"); // seed the running low payload word + emitter.instruction("str x2, [sp, #48]"); // seed the running high payload word + emitter.instruction("mov x9, #1"); // the reduction resumes at the second element + emitter.instruction("str x9, [sp, #16]"); // save the element cursor + + // -- fold every remaining element into the running result -- + emitter.label("__rt_mmm_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the element cursor + emitter.instruction("ldr x10, [sp, #8]"); // reload the element count + emitter.instruction("cmp x9, x10"); // has every element been folded in? + emitter.instruction("b.ge __rt_mmm_done"); // finish once the payload slots are exhausted + emitter.instruction("ldr x10, [sp, #0]"); // reload the source indexed-array pointer + emitter.instruction(&format!("add x10, x10, #{}", ARRAY_DATA_OFFSET)); // advance from the header to the payload slots + emitter.instruction("ldr x0, [x10, x9, lsl #3]"); // load the borrowed Mixed cell the cursor points at + emitter.instruction("bl __rt_mixed_unbox"); // peel the candidate into a concrete tag/lo/hi triple + emitter.instruction("str x0, [sp, #56]"); // save the candidate runtime tag + emitter.instruction("str x1, [sp, #64]"); // save the candidate low payload word + emitter.instruction("str x2, [sp, #72]"); // save the candidate high payload word + emitter.instruction("ldr x3, [sp, #32]"); // pass the running runtime tag as the right operand + emitter.instruction("ldr x4, [sp, #40]"); // pass the running low payload word + emitter.instruction("ldr x5, [sp, #48]"); // pass the running high payload word + emitter.instruction("bl __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("ldr x9, [sp, #24]"); // reload the min/max direction flag + emitter.instruction("cbz x9, __rt_mmm_want_min"); // min() keeps the smaller element + emitter.instruction("cmp x0, #0"); // did the candidate compare greater than the result? + emitter.instruction("b.gt __rt_mmm_take"); // max() only replaces on a strict win, so ties keep the earlier element + emitter.instruction("b __rt_mmm_next"); // otherwise keep the running result + emitter.label("__rt_mmm_want_min"); + emitter.instruction("cmp x0, #0"); // did the candidate compare smaller than the result? + emitter.instruction("b.ge __rt_mmm_next"); // min() only replaces on a strict win + emitter.label("__rt_mmm_take"); + emitter.instruction("ldr x9, [sp, #56]"); // reload the winning candidate tag + emitter.instruction("str x9, [sp, #32]"); // publish it as the new running tag + emitter.instruction("ldr x9, [sp, #64]"); // reload the winning candidate low payload word + emitter.instruction("str x9, [sp, #40]"); // publish it as the new running low word + emitter.instruction("ldr x9, [sp, #72]"); // reload the winning candidate high payload word + emitter.instruction("str x9, [sp, #48]"); // publish it as the new running high word + emitter.label("__rt_mmm_next"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the element cursor + emitter.instruction("add x9, x9, #1"); // advance to the next payload slot + emitter.instruction("str x9, [sp, #16]"); // save the advanced cursor + emitter.instruction("b __rt_mmm_loop"); // continue the reduction + + emitter.label("__rt_mmm_done"); + emitter.instruction("ldr x0, [sp, #32]"); // return the winning runtime tag + emitter.instruction("ldr x1, [sp, #40]"); // return the winning low payload word + emitter.instruction("ldr x2, [sp, #48]"); // return the winning high payload word + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the reduction frame + emitter.instruction("ret"); // return the reduced element triple +} + +/// Emits the Linux x86_64 System V implementation of the boxed-Mixed reduction. +fn emit_min_max_mixed_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: min_max_mixed ---"); + emitter.label_global("__rt_min_max_mixed"); + + // Frame (80 bytes below rbp): [-8]=array [-16]=length [-24]=cursor [-32]=want_max + // [-40..-56]=best tag/lo/hi [-64..-80]=candidate tag/lo/hi + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the reduction frame pointer + emitter.instruction("sub rsp, 80"); // allocate the aligned reduction frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source indexed-array pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the min/max direction flag + emitter.instruction("mov QWORD PTR [rbp - 40], -1"); // tag -1 reports "the container yielded no element" + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear the running low payload word + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // clear the running high payload word + emitter.instruction("test rdi, rdi"); // is the container pointer null? + emitter.instruction("jz __rt_mmm_done_x86"); // a null container behaves like an empty array + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the array's logical element count from its header + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the element count across the helper calls + emitter.instruction("test rax, rax"); // does the array hold any element? + emitter.instruction("jz __rt_mmm_done_x86"); // an empty array yields the sentinel tag + + // -- seed the reduction with the first element -- + emitter.instruction(&format!("mov rax, QWORD PTR [rdi + {}]", ARRAY_DATA_OFFSET)); // load the borrowed Mixed cell of element zero + emitter.instruction("call __rt_mixed_unbox"); // peel the cell into a concrete tag/lo/hi triple + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // seed the running runtime tag + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // seed the running low payload word + emitter.instruction("mov QWORD PTR [rbp - 56], rdx"); // seed the running high payload word + emitter.instruction("mov QWORD PTR [rbp - 24], 1"); // the reduction resumes at the second element + + // -- fold every remaining element into the running result -- + emitter.label("__rt_mmm_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the element cursor + emitter.instruction("cmp r10, QWORD PTR [rbp - 16]"); // has every element been folded in? + emitter.instruction("jge __rt_mmm_done_x86"); // finish once the payload slots are exhausted + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the source indexed-array pointer + emitter.instruction(&format!("mov rax, QWORD PTR [rax + r10 * 8 + {}]", ARRAY_DATA_OFFSET)); // load the borrowed Mixed cell the cursor points at + emitter.instruction("call __rt_mixed_unbox"); // peel the candidate into a concrete tag/lo/hi triple + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the candidate runtime tag + emitter.instruction("mov QWORD PTR [rbp - 72], rdi"); // save the candidate low payload word + emitter.instruction("mov QWORD PTR [rbp - 80], rdx"); // save the candidate high payload word + emitter.instruction("mov rdi, rax"); // pass the candidate runtime tag as the left operand + emitter.instruction("mov rsi, QWORD PTR [rbp - 72]"); // pass the candidate low payload word + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // pass the candidate high payload word + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // pass the running runtime tag as the right operand + emitter.instruction("mov r8, QWORD PTR [rbp - 48]"); // pass the running low payload word + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // pass the running high payload word + emitter.instruction("call __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // reload the min/max direction flag + emitter.instruction("je __rt_mmm_want_min_x86"); // min() keeps the smaller element + emitter.instruction("cmp rax, 0"); // did the candidate compare greater than the result? + emitter.instruction("jg __rt_mmm_take_x86"); // max() only replaces on a strict win, so ties keep the earlier element + emitter.instruction("jmp __rt_mmm_next_x86"); // otherwise keep the running result + emitter.label("__rt_mmm_want_min_x86"); + emitter.instruction("cmp rax, 0"); // did the candidate compare smaller than the result? + emitter.instruction("jge __rt_mmm_next_x86"); // min() only replaces on a strict win + emitter.label("__rt_mmm_take_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the winning candidate tag + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // publish it as the new running tag + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the winning candidate low payload word + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // publish it as the new running low word + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload the winning candidate high payload word + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // publish it as the new running high word + emitter.label("__rt_mmm_next_x86"); + emitter.instruction("add QWORD PTR [rbp - 24], 1"); // advance the cursor to the next payload slot + emitter.instruction("jmp __rt_mmm_loop_x86"); // continue the reduction + + emitter.label("__rt_mmm_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the winning runtime tag + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // return the winning low payload word + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // return the winning high payload word + emitter.instruction("mov rsp, rbp"); // release the reduction frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the reduced element triple +} + +/// Emits `__rt_min_max_str`: reduces an indexed array of PHP byte strings. +/// +/// Indexed string arrays use 16-byte payload slots (`[ptr:8][len:8]`), so the +/// element is already an unboxed string triple with runtime tag 1. Input and +/// output registers match `__rt_min_max_mixed`; the returned pointer is borrowed +/// from the array's own payload slot. +pub fn emit_min_max_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_min_max_str_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: min_max_str ---"); + emitter.label_global("__rt_min_max_str"); + + // Frame (96 bytes): [0]=array [8]=length [16]=cursor [24]=want_max + // [32..48]=best tag/ptr/len [56..72]=candidate tag/ptr/len [80]=x29/x30 + emitter.instruction("sub sp, sp, #96"); // allocate the reduction frame + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the reduction frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the source indexed-array pointer + emitter.instruction("str x1, [sp, #24]"); // save the min/max direction flag + emitter.instruction("mov x9, #-1"); // tag -1 reports "the container yielded no element" + emitter.instruction("str x9, [sp, #32]"); // seed the running result with the empty sentinel + emitter.instruction("str xzr, [sp, #40]"); // clear the running string pointer + emitter.instruction("str xzr, [sp, #48]"); // clear the running string length + emitter.instruction("cbz x0, __rt_mms_done"); // a null container behaves like an empty array + emitter.instruction("ldr x9, [x0]"); // load the array's logical element count from its header + emitter.instruction("str x9, [sp, #8]"); // preserve the element count across the helper calls + emitter.instruction("cbz x9, __rt_mms_done"); // an empty array yields the sentinel tag + + // -- seed the reduction with the first string slot -- + emitter.instruction(&format!("add x10, x0, #{}", ARRAY_DATA_OFFSET)); // advance from the header to the 16-byte string slots + emitter.instruction("ldr x11, [x10]"); // load the first element's borrowed string pointer + emitter.instruction("ldr x12, [x10, #8]"); // load the first element's string length + emitter.instruction("mov x9, #1"); // runtime tag 1 = string + emitter.instruction("str x9, [sp, #32]"); // seed the running runtime tag + emitter.instruction("str x11, [sp, #40]"); // seed the running string pointer + emitter.instruction("str x12, [sp, #48]"); // seed the running string length + emitter.instruction("str x9, [sp, #16]"); // the reduction resumes at the second element + + // -- fold every remaining string into the running result -- + emitter.label("__rt_mms_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the element cursor + emitter.instruction("ldr x10, [sp, #8]"); // reload the element count + emitter.instruction("cmp x9, x10"); // has every element been folded in? + emitter.instruction("b.ge __rt_mms_done"); // finish once the payload slots are exhausted + emitter.instruction("ldr x10, [sp, #0]"); // reload the source indexed-array pointer + emitter.instruction(&format!("add x10, x10, #{}", ARRAY_DATA_OFFSET)); // advance from the header to the string slots + emitter.instruction("add x10, x10, x9, lsl #4"); // address the 16-byte slot the cursor points at + emitter.instruction("mov x0, #1"); // the candidate is a string + emitter.instruction("ldr x1, [x10]"); // load the candidate's borrowed string pointer + emitter.instruction("ldr x2, [x10, #8]"); // load the candidate's string length + emitter.instruction("str x0, [sp, #56]"); // save the candidate runtime tag + emitter.instruction("str x1, [sp, #64]"); // save the candidate string pointer + emitter.instruction("str x2, [sp, #72]"); // save the candidate string length + emitter.instruction("ldr x3, [sp, #32]"); // pass the running runtime tag as the right operand + emitter.instruction("ldr x4, [sp, #40]"); // pass the running string pointer + emitter.instruction("ldr x5, [sp, #48]"); // pass the running string length + emitter.instruction("bl __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("ldr x9, [sp, #24]"); // reload the min/max direction flag + emitter.instruction("cbz x9, __rt_mms_want_min"); // min() keeps the smaller element + emitter.instruction("cmp x0, #0"); // did the candidate compare greater than the result? + emitter.instruction("b.gt __rt_mms_take"); // max() only replaces on a strict win, so ties keep the earlier element + emitter.instruction("b __rt_mms_next"); // otherwise keep the running result + emitter.label("__rt_mms_want_min"); + emitter.instruction("cmp x0, #0"); // did the candidate compare smaller than the result? + emitter.instruction("b.ge __rt_mms_next"); // min() only replaces on a strict win + emitter.label("__rt_mms_take"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the winning candidate string pointer + emitter.instruction("str x9, [sp, #40]"); // publish it as the new running string pointer + emitter.instruction("ldr x9, [sp, #72]"); // reload the winning candidate string length + emitter.instruction("str x9, [sp, #48]"); // publish it as the new running string length + emitter.label("__rt_mms_next"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the element cursor + emitter.instruction("add x9, x9, #1"); // advance to the next string slot + emitter.instruction("str x9, [sp, #16]"); // save the advanced cursor + emitter.instruction("b __rt_mms_loop"); // continue the reduction + + emitter.label("__rt_mms_done"); + emitter.instruction("ldr x0, [sp, #32]"); // return the winning runtime tag + emitter.instruction("ldr x1, [sp, #40]"); // return the winning string pointer + emitter.instruction("ldr x2, [sp, #48]"); // return the winning string length + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the reduction frame + emitter.instruction("ret"); // return the reduced element triple +} + +/// Emits the Linux x86_64 System V implementation of the indexed-string reduction. +fn emit_min_max_str_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: min_max_str ---"); + emitter.label_global("__rt_min_max_str"); + + // Frame (80 bytes below rbp): [-8]=array [-16]=length [-24]=cursor [-32]=want_max + // [-40..-56]=best tag/ptr/len [-64..-80]=candidate tag/ptr/len + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the reduction frame pointer + emitter.instruction("sub rsp, 80"); // allocate the aligned reduction frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source indexed-array pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the min/max direction flag + emitter.instruction("mov QWORD PTR [rbp - 40], -1"); // tag -1 reports "the container yielded no element" + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear the running string pointer + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // clear the running string length + emitter.instruction("test rdi, rdi"); // is the container pointer null? + emitter.instruction("jz __rt_mms_done_x86"); // a null container behaves like an empty array + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the array's logical element count from its header + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the element count across the helper calls + emitter.instruction("test rax, rax"); // does the array hold any element? + emitter.instruction("jz __rt_mms_done_x86"); // an empty array yields the sentinel tag + + // -- seed the reduction with the first string slot -- + emitter.instruction(&format!("lea r10, [rdi + {}]", ARRAY_DATA_OFFSET)); // advance from the header to the 16-byte string slots + emitter.instruction("mov QWORD PTR [rbp - 40], 1"); // runtime tag 1 = string + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the first element's borrowed string pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // seed the running string pointer + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // load the first element's string length + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // seed the running string length + emitter.instruction("mov QWORD PTR [rbp - 24], 1"); // the reduction resumes at the second element + + // -- fold every remaining string into the running result -- + emitter.label("__rt_mms_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the element cursor + emitter.instruction("cmp r10, QWORD PTR [rbp - 16]"); // has every element been folded in? + emitter.instruction("jge __rt_mms_done_x86"); // finish once the payload slots are exhausted + emitter.instruction("shl r10, 4"); // convert the cursor into a 16-byte slot offset + emitter.instruction("add r10, QWORD PTR [rbp - 8]"); // address the payload slot inside the source array + emitter.instruction(&format!("add r10, {}", ARRAY_DATA_OFFSET)); // skip the indexed-array header + emitter.instruction("mov rdi, 1"); // the candidate is a string + emitter.instruction("mov rsi, QWORD PTR [r10]"); // load the candidate's borrowed string pointer + emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the candidate's string length + emitter.instruction("mov QWORD PTR [rbp - 72], rsi"); // save the candidate string pointer + emitter.instruction("mov QWORD PTR [rbp - 80], rdx"); // save the candidate string length + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // pass the running runtime tag as the right operand + emitter.instruction("mov r8, QWORD PTR [rbp - 48]"); // pass the running string pointer + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // pass the running string length + emitter.instruction("call __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // reload the min/max direction flag + emitter.instruction("je __rt_mms_want_min_x86"); // min() keeps the smaller element + emitter.instruction("cmp rax, 0"); // did the candidate compare greater than the result? + emitter.instruction("jg __rt_mms_take_x86"); // max() only replaces on a strict win, so ties keep the earlier element + emitter.instruction("jmp __rt_mms_next_x86"); // otherwise keep the running result + emitter.label("__rt_mms_want_min_x86"); + emitter.instruction("cmp rax, 0"); // did the candidate compare smaller than the result? + emitter.instruction("jge __rt_mms_next_x86"); // min() only replaces on a strict win + emitter.label("__rt_mms_take_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the winning candidate string pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // publish it as the new running string pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload the winning candidate string length + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // publish it as the new running string length + emitter.label("__rt_mms_next_x86"); + emitter.instruction("add QWORD PTR [rbp - 24], 1"); // advance the cursor to the next string slot + emitter.instruction("jmp __rt_mms_loop_x86"); // continue the reduction + + emitter.label("__rt_mms_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the winning runtime tag + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // return the winning string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // return the winning string length + emitter.instruction("mov rsp, rbp"); // release the reduction frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the reduced element triple +} + +/// Emits `__rt_min_max_hash`: reduces a hash-backed associative array's values. +/// +/// Walks the table in insertion order through `__rt_hash_iter_next`, normalizing +/// boxed entries (runtime tag 7) with `__rt_mixed_unbox` so values of any type +/// reach `__rt_php_compare` as a concrete triple. Input and output registers +/// match `__rt_min_max_mixed`; string payloads stay borrowed from the table. +pub fn emit_min_max_hash(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_min_max_hash_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: min_max_hash ---"); + emitter.label_global("__rt_min_max_hash"); + + // Frame (96 bytes): [0]=hash [8]=cursor [16]=want_max + // [24..40]=best tag/lo/hi [48..64]=candidate tag/lo/hi [80]=x29/x30 + emitter.instruction("sub sp, sp, #96"); // allocate the reduction frame + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the reduction frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the source associative-array pointer + emitter.instruction("str x1, [sp, #16]"); // save the min/max direction flag + emitter.instruction("str xzr, [sp, #8]"); // start the insertion-order walk at cursor zero + emitter.instruction("mov x9, #-1"); // tag -1 reports "the container yielded no element" + emitter.instruction("str x9, [sp, #24]"); // seed the running result with the empty sentinel + emitter.instruction("str xzr, [sp, #32]"); // clear the running low payload word + emitter.instruction("str xzr, [sp, #40]"); // clear the running high payload word + + // -- visit every value in insertion order -- + emitter.label("__rt_mmh_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the source hash pointer for the iterator + emitter.instruction("ldr x1, [sp, #8]"); // reload the insertion-order cursor + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next entry with cursor x0, payload x3/x4, and tag x5 + emitter.instruction("cmn x0, #1"); // did the iterator return its terminal negative-one cursor? + emitter.instruction("b.eq __rt_mmh_done"); // finish once every entry has been folded in + emitter.instruction("str x0, [sp, #8]"); // preserve the next insertion-order cursor + emitter.instruction("cmp x5, #7"); // does the entry hold a boxed Mixed cell? + emitter.instruction("b.ne __rt_mmh_direct"); // unboxed entries already carry a concrete triple + emitter.instruction("mov x0, x3"); // pass the borrowed Mixed cell to the unboxing helper + emitter.instruction("bl __rt_mixed_unbox"); // peel the cell into a concrete tag/lo/hi triple + emitter.instruction("b __rt_mmh_candidate"); // continue with the normalized candidate + emitter.label("__rt_mmh_direct"); + emitter.instruction("mov x0, x5"); // the entry's runtime tag is already concrete + emitter.instruction("mov x1, x3"); // the entry's low payload word + emitter.instruction("mov x2, x4"); // the entry's high payload word + emitter.label("__rt_mmh_candidate"); + emitter.instruction("str x0, [sp, #48]"); // save the candidate runtime tag + emitter.instruction("str x1, [sp, #56]"); // save the candidate low payload word + emitter.instruction("str x2, [sp, #64]"); // save the candidate high payload word + emitter.instruction("ldr x9, [sp, #24]"); // reload the running runtime tag + emitter.instruction("cmn x9, #1"); // is this the first value the walk has seen? + emitter.instruction("b.eq __rt_mmh_take"); // the first value seeds the reduction unconditionally + emitter.instruction("ldr x3, [sp, #24]"); // pass the running runtime tag as the right operand + emitter.instruction("ldr x4, [sp, #32]"); // pass the running low payload word + emitter.instruction("ldr x5, [sp, #40]"); // pass the running high payload word + emitter.instruction("bl __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("ldr x9, [sp, #16]"); // reload the min/max direction flag + emitter.instruction("cbz x9, __rt_mmh_want_min"); // min() keeps the smaller value + emitter.instruction("cmp x0, #0"); // did the candidate compare greater than the result? + emitter.instruction("b.gt __rt_mmh_take"); // max() only replaces on a strict win, so ties keep the earlier value + emitter.instruction("b __rt_mmh_loop"); // otherwise keep the running result + emitter.label("__rt_mmh_want_min"); + emitter.instruction("cmp x0, #0"); // did the candidate compare smaller than the result? + emitter.instruction("b.ge __rt_mmh_loop"); // min() only replaces on a strict win + emitter.label("__rt_mmh_take"); + emitter.instruction("ldr x9, [sp, #48]"); // reload the winning candidate tag + emitter.instruction("str x9, [sp, #24]"); // publish it as the new running tag + emitter.instruction("ldr x9, [sp, #56]"); // reload the winning candidate low payload word + emitter.instruction("str x9, [sp, #32]"); // publish it as the new running low word + emitter.instruction("ldr x9, [sp, #64]"); // reload the winning candidate high payload word + emitter.instruction("str x9, [sp, #40]"); // publish it as the new running high word + emitter.instruction("b __rt_mmh_loop"); // continue with the next insertion-order entry + + emitter.label("__rt_mmh_done"); + emitter.instruction("ldr x0, [sp, #24]"); // return the winning runtime tag + emitter.instruction("ldr x1, [sp, #32]"); // return the winning low payload word + emitter.instruction("ldr x2, [sp, #40]"); // return the winning high payload word + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the reduction frame + emitter.instruction("ret"); // return the reduced value triple +} + +/// Emits the Linux x86_64 System V implementation of the associative-array reduction. +fn emit_min_max_hash_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: min_max_hash ---"); + emitter.label_global("__rt_min_max_hash"); + + // Frame (80 bytes below rbp): [-8]=hash [-16]=cursor [-24]=want_max + // [-32..-48]=best tag/lo/hi [-56..-72]=candidate tag/lo/hi + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the reduction frame pointer + emitter.instruction("sub rsp, 80"); // allocate the aligned reduction frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source associative-array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // save the min/max direction flag + emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // start the insertion-order walk at cursor zero + emitter.instruction("mov QWORD PTR [rbp - 32], -1"); // tag -1 reports "the container yielded no element" + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // clear the running low payload word + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear the running high payload word + + // -- visit every value in insertion order -- + emitter.label("__rt_mmh_loop_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the source hash pointer for the iterator + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the insertion-order cursor + emitter.instruction("call __rt_hash_iter_next"); // fetch the next entry with cursor rax, payload rcx/r8, and tag r9 + emitter.instruction("cmp rax, -1"); // did the iterator return its terminal cursor? + emitter.instruction("je __rt_mmh_done_x86"); // finish once every entry has been folded in + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // preserve the next insertion-order cursor + emitter.instruction("cmp r9, 7"); // does the entry hold a boxed Mixed cell? + emitter.instruction("jne __rt_mmh_direct_x86"); // unboxed entries already carry a concrete triple + emitter.instruction("mov rax, rcx"); // pass the borrowed Mixed cell to the unboxing helper + emitter.instruction("call __rt_mixed_unbox"); // peel the cell into a concrete tag/lo/hi triple + emitter.instruction("jmp __rt_mmh_candidate_x86"); // continue with the normalized candidate + emitter.label("__rt_mmh_direct_x86"); + emitter.instruction("mov rax, r9"); // the entry's runtime tag is already concrete + emitter.instruction("mov rdi, rcx"); // the entry's low payload word + emitter.instruction("mov rdx, r8"); // the entry's high payload word + emitter.label("__rt_mmh_candidate_x86"); + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the candidate runtime tag + emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // save the candidate low payload word + emitter.instruction("mov QWORD PTR [rbp - 72], rdx"); // save the candidate high payload word + emitter.instruction("cmp QWORD PTR [rbp - 32], -1"); // is this the first value the walk has seen? + emitter.instruction("je __rt_mmh_take_x86"); // the first value seeds the reduction unconditionally + emitter.instruction("mov rdi, rax"); // pass the candidate runtime tag as the left operand + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // pass the candidate low payload word + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // pass the candidate high payload word + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // pass the running runtime tag as the right operand + emitter.instruction("mov r8, QWORD PTR [rbp - 40]"); // pass the running low payload word + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // pass the running high payload word + emitter.instruction("call __rt_php_compare"); // apply PHP 8's ordering table to candidate versus result + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // reload the min/max direction flag + emitter.instruction("je __rt_mmh_want_min_x86"); // min() keeps the smaller value + emitter.instruction("cmp rax, 0"); // did the candidate compare greater than the result? + emitter.instruction("jg __rt_mmh_take_x86"); // max() only replaces on a strict win, so ties keep the earlier value + emitter.instruction("jmp __rt_mmh_loop_x86"); // otherwise keep the running result + emitter.label("__rt_mmh_want_min_x86"); + emitter.instruction("cmp rax, 0"); // did the candidate compare smaller than the result? + emitter.instruction("jge __rt_mmh_loop_x86"); // min() only replaces on a strict win + emitter.label("__rt_mmh_take_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the winning candidate tag + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // publish it as the new running tag + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the winning candidate low payload word + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // publish it as the new running low word + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the winning candidate high payload word + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // publish it as the new running high word + emitter.instruction("jmp __rt_mmh_loop_x86"); // continue with the next insertion-order entry + + emitter.label("__rt_mmh_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the winning runtime tag + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // return the winning low payload word + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // return the winning high payload word + emitter.instruction("mov rsp, rbp"); // release the reduction frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the reduced value triple +} diff --git a/src/codegen_support/runtime/arrays/mixed_abs.rs b/src/codegen_support/runtime/arrays/mixed_abs.rs index 91419f1775..277e711de9 100644 --- a/src/codegen_support/runtime/arrays/mixed_abs.rs +++ b/src/codegen_support/runtime/arrays/mixed_abs.rs @@ -44,11 +44,19 @@ pub fn emit_mixed_abs(emitter: &mut Emitter) { emitter.label("__rt_abs_mixed_int"); emitter.instruction("cmp x1, #0"); // compare the integer payload against zero emitter.instruction("cneg x1, x1, lt"); // negate the integer only when it was negative + emitter.instruction("tbnz x1, #63, __rt_abs_mixed_int_overflow"); // only PHP_INT_MIN stays negative: PHP promotes that one to float emitter.instruction("mov x0, #0"); // runtime tag 0 = integer emitter.instruction("mov x2, #0"); // integer payloads do not use a high word emitter.instruction("bl __rt_mixed_from_value"); // box the integer absolute value into a Mixed cell emitter.instruction("b __rt_abs_mixed_done"); // return the boxed integer result + emitter.label("__rt_abs_mixed_int_overflow"); + emitter.instruction("movz x1, #0x43e0, lsl #48"); // 9223372036854775808.0 (2^63) as IEEE-754 bits + emitter.instruction("mov x0, #2"); // runtime tag 2 = float + emitter.instruction("mov x2, #0"); // float payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box abs(PHP_INT_MIN) as the promoted float like PHP + emitter.instruction("b __rt_abs_mixed_done"); // return the boxed float result + emitter.label("__rt_abs_mixed_float"); emitter.instruction("fmov d0, x1"); // move the unboxed float bits into the FP register file emitter.instruction("fabs d0, d0"); // take the floating-point absolute value @@ -91,11 +99,20 @@ fn emit_mixed_abs_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sar r10, 63"); // expand the sign bit into an all-zero or all-one mask emitter.instruction("xor rdi, r10"); // flip the payload bits when the integer was negative emitter.instruction("sub rdi, r10"); // subtract the sign mask to finish the two's-complement absolute value + emitter.instruction("test rdi, rdi"); // inspect the sign of the computed magnitude + emitter.instruction("js __rt_abs_mixed_int_overflow_x86"); // only PHP_INT_MIN stays negative: PHP promotes that one to float emitter.instruction("xor rsi, rsi"); // integer payloads do not use a high word emitter.instruction("mov rax, 0"); // runtime tag 0 = integer emitter.instruction("call __rt_mixed_from_value"); // box the integer absolute value into a Mixed cell emitter.instruction("jmp __rt_abs_mixed_done_x86"); // return the boxed integer result + emitter.label("__rt_abs_mixed_int_overflow_x86"); + emitter.instruction("mov rdi, 0x43e0000000000000"); // 9223372036854775808.0 (2^63) as IEEE-754 bits + emitter.instruction("xor rsi, rsi"); // float payloads do not use a high word + emitter.instruction("mov rax, 2"); // runtime tag 2 = float + emitter.instruction("call __rt_mixed_from_value"); // box abs(PHP_INT_MIN) as the promoted float like PHP + emitter.instruction("jmp __rt_abs_mixed_done_x86"); // return the boxed float result + emitter.label("__rt_abs_mixed_float_x86"); emitter.instruction("mov r11, 0x7fffffffffffffff"); // materialize a mask that clears the IEEE-754 sign bit emitter.instruction("and rdi, r11"); // clear the sign bit so the float payload becomes its absolute value diff --git a/src/codegen_support/runtime/arrays/mixed_cast_bool.rs b/src/codegen_support/runtime/arrays/mixed_cast_bool.rs index f57a11bb87..ef4f22ae1b 100644 --- a/src/codegen_support/runtime/arrays/mixed_cast_bool.rs +++ b/src/codegen_support/runtime/arrays/mixed_cast_bool.rs @@ -20,7 +20,9 @@ use crate::codegen_support::platform::Arch; /// and null/unsupported (falsy). Calls `__rt_mixed_unbox` to拆box the input pointer. /// /// ABI: ARM64 — input boxed mixed pointer in `x0`, result boolean in `x0`. -/// ABI: x86_64 — input boxed mixed pointer in `rdi`, result boolean in `rax`. +/// ABI: x86_64 — input boxed mixed pointer in `rax`, result boolean in `rax`. The input +/// register is `rax` and not the SysV first argument register because the boxed cell is +/// forwarded untouched to `__rt_mixed_unbox`, which reads it from `rax`. pub fn emit_mixed_cast_bool(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_mixed_cast_bool_linux_x86_64(emitter); @@ -105,9 +107,10 @@ pub fn emit_mixed_cast_bool(emitter: &mut Emitter) { } /// Emits the `__rt_mixed_cast_bool` runtime helper for the x86_64 Linux target. -/// Mirrors the ARM64 logic with x86_64 SysV ABI register conventions: -/// input boxed mixed pointer in `rdi`, result boolean in `rax`. -/// Uses `__rt_mixed_unbox` to拆box the input before tag-based dispatch. +/// Mirrors the ARM64 logic: input boxed mixed pointer in `rax`, result boolean in `rax`. +/// The input arrives in `rax` rather than in `rdi` because it is passed straight through to +/// `__rt_mixed_unbox`, whose x86_64 input register is `rax`; callers that also set `rdi` +/// merely happen to leave `rax` live from `load_value_to_first_int_arg`. fn emit_mixed_cast_bool_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: mixed_cast_bool ---"); diff --git a/src/codegen_support/runtime/arrays/mixed_cast_float.rs b/src/codegen_support/runtime/arrays/mixed_cast_float.rs index c68a411bc2..0d7dd99799 100644 --- a/src/codegen_support/runtime/arrays/mixed_cast_float.rs +++ b/src/codegen_support/runtime/arrays/mixed_cast_float.rs @@ -71,9 +71,9 @@ pub fn emit_mixed_cast_float(emitter: &mut Emitter) { emitter.instruction("ret"); // return the floating-point cast result in d0 } -/// x86_64 Linux SysV ABI variant of `emit_mixed_cast_float`. Uses the SysV calling -/// convention: -/// - Input: rdi = boxed mixed pointer +/// x86_64 Linux variant of `emit_mixed_cast_float`: +/// - Input: rax = boxed mixed pointer. The cell is forwarded untouched to +/// `__rt_mixed_unbox`, whose x86_64 input register is `rax`, so `rdi` is NOT the input. /// - Tag returned in rax, payload words in rdi/rdx after `__rt_mixed_unbox` /// - Float result returned in xmm0 /// - Stack kept 16-byte aligned; one 16-byte scratch slot reserved for nested calls. diff --git a/src/codegen_support/runtime/arrays/mixed_cast_int.rs b/src/codegen_support/runtime/arrays/mixed_cast_int.rs index 7236e1bd78..cf7513b385 100644 --- a/src/codegen_support/runtime/arrays/mixed_cast_int.rs +++ b/src/codegen_support/runtime/arrays/mixed_cast_int.rs @@ -67,7 +67,7 @@ pub fn emit_mixed_cast_int(emitter: &mut Emitter) { emitter.label("__rt_mixed_cast_int_from_float"); emitter.instruction("fmov d0, x1"); // move the unboxed float bits into the FP register file - emitter.instruction("fcvtzs x0, d0"); // truncate the float payload toward zero + abi::emit_php_float_to_int(emitter, "x0"); // apply PHP float->int rules to the unboxed float payload emitter.instruction("b __rt_mixed_cast_int_done"); // return the converted integer result emitter.label("__rt_mixed_cast_int_from_bool"); @@ -140,7 +140,7 @@ fn emit_mixed_cast_int_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_mixed_cast_int_from_float_linux_x86_64"); emitter.instruction("movq xmm0, rdi"); // move the unboxed float bits into the floating-point result register - emitter.instruction("cvttsd2si rax, xmm0"); // truncate the floating-point payload toward zero + abi::emit_php_float_to_int(emitter, "rax"); // apply PHP float->int rules to the unboxed float payload emitter.instruction("jmp __rt_mixed_cast_int_done_linux_x86_64"); // return the converted integer result emitter.label("__rt_mixed_cast_int_from_bool_linux_x86_64"); diff --git a/src/codegen_support/runtime/arrays/mixed_intval_base.rs b/src/codegen_support/runtime/arrays/mixed_intval_base.rs new file mode 100644 index 0000000000..02e54ac679 --- /dev/null +++ b/src/codegen_support/runtime/arrays/mixed_intval_base.rs @@ -0,0 +1,102 @@ +//! Purpose: +//! Emits the `__rt_mixed_intval_base` runtime helper: PHP `intval($value, $base)` applied to a +//! boxed `Mixed` cell whose payload type is only known at run time. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - php-src's `PHP_FUNCTION(intval)` honors `$base` only when the subject is a string and +//! otherwise behaves exactly like a plain `(int)` cast, so this helper unboxes the cell, +//! routes a tag-1 string payload to `__rt_str_to_int_base`, and hands every other payload +//! to `__rt_mixed_cast_int` unchanged. +//! - The boxed pointer is saved before the unbox because the fallback path needs the original +//! cell, and the base is saved because `__rt_mixed_unbox` owns the argument registers. +//! - Mixed helpers use boxed tag/payload cells; tag constants and ownership rules are shared +//! with type checking and codegen. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_mixed_intval_base` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x0` = boxed mixed pointer, `x3` = requested base. +/// Output: `x0` = the PHP integer value. +/// +/// ABI (x86_64 System V): +/// Input: `rax` = boxed mixed pointer, `rcx` = requested base. +/// Output: `rax` = the PHP integer value. +/// +/// The input registers mirror `__rt_mixed_cast_int`, which this helper tail-calls for every +/// payload PHP's `$base` does not apply to. +pub fn emit_mixed_intval_base(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mixed_intval_base_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: mixed_intval_base ---"); + emitter.label_global("__rt_mixed_intval_base"); + + emitter.instruction("sub sp, sp, #32"); // allocate a small stack frame for the nested helper calls + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish the helper stack frame + emitter.instruction("str x0, [sp]"); // keep the boxed pointer for the non-string fallback + emitter.instruction("str x3, [sp, #8]"); // keep the requested base across the unbox call + emitter.instruction("bl __rt_mixed_unbox"); // x0=tag, x1=value_lo, x2=value_hi for the boxed payload + emitter.instruction("cmp x0, #1"); // does the mixed payload hold a string? + emitter.instruction("b.ne __rt_mixed_intval_base_cast"); // every other payload ignores PHP's $base argument + emitter.instruction("ldr x3, [sp, #8]"); // restore the requested base for the string parser + emitter.instruction("bl __rt_str_to_int_base"); // parse the unboxed string payload in the requested base + emitter.instruction("b __rt_mixed_intval_base_done"); // return the parsed integer result + + emitter.label("__rt_mixed_intval_base_cast"); + emitter.instruction("ldr x0, [sp]"); // restore the boxed pointer for the ordinary integer cast + emitter.instruction("bl __rt_mixed_cast_int"); // non-string payloads cast exactly like one-argument intval() + + emitter.label("__rt_mixed_intval_base_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper stack frame + emitter.instruction("ret"); // return the integer result in x0 +} + +/// Emits the x86_64 Linux variant of `__rt_mixed_intval_base`. +/// +/// `__rt_mixed_unbox` returns the tag in `rax` and the payload words in `rdi`/`rdx` here, so a +/// string payload already has its pointer in `rdi` and only needs its length moved into `rsi` +/// before the base parser's System V argument list is complete. +/// +/// # ABI +/// - Input: rax = boxed mixed pointer, rcx = requested base +/// - Output: rax = integer result +/// - Clobbers: rax, rcx, rdi, rsi, rdx, xmm0, rsp; preserves rbp +fn emit_mixed_intval_base_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_intval_base ---"); + emitter.label_global("__rt_mixed_intval_base"); + + emitter.instruction("push rbp"); // save the caller frame pointer before this helper allocates its own frame + emitter.instruction("mov rbp, rsp"); // establish a stable frame pointer for the helper body + emitter.instruction("sub rsp, 16"); // reserve one aligned temporary slot so nested helper calls keep the SysV stack aligned + emitter.instruction("mov QWORD PTR [rsp], rax"); // keep the boxed pointer for the non-string fallback + emitter.instruction("mov QWORD PTR [rsp + 8], rcx"); // keep the requested base across the unbox call + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // return the mixed runtime tag in rax and payload words in rdi/rdx + emitter.instruction("cmp rax, 1"); // does the mixed payload hold a string? + emitter.instruction("jne __rt_mixed_intval_base_cast_linux_x86_64"); // every other payload ignores PHP's $base argument + emitter.instruction("mov rsi, rdx"); // move the unboxed string length into the parser's second argument + emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // restore the requested base for the string parser + abi::emit_call_label(emitter, "__rt_str_to_int_base"); // parse the unboxed string payload in the requested base + emitter.instruction("jmp __rt_mixed_intval_base_done_linux_x86_64"); // return the parsed integer result + + emitter.label("__rt_mixed_intval_base_cast_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rsp]"); // restore the boxed pointer for the ordinary integer cast + abi::emit_call_label(emitter, "__rt_mixed_cast_int"); // non-string payloads cast exactly like one-argument intval() + + emitter.label("__rt_mixed_intval_base_done_linux_x86_64"); + emitter.instruction("add rsp, 16"); // release the aligned temporary slot reserved for nested helper calls + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning + emitter.instruction("ret"); // return the integer result in rax +} diff --git a/src/codegen_support/runtime/arrays/mixed_numeric_pow.rs b/src/codegen_support/runtime/arrays/mixed_numeric_pow.rs new file mode 100644 index 0000000000..0bd26b7ee3 --- /dev/null +++ b/src/codegen_support/runtime/arrays/mixed_numeric_pow.rs @@ -0,0 +1,159 @@ +//! Purpose: +//! Emits `__rt_mixed_numeric_pow`, PHP's `**` over two boxed Mixed operands. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::arrays`. +//! - Generated code through `Op::MixedNumericBinop` with the `MixedNumericOp::Pow` +//! immediate (`crate::codegen::lower_inst::arithmetic`). +//! +//! Key details: +//! - `**` is the only mixed numeric operator that is NOT add/sub/mul-shaped: the integer +//! path is a square-and-multiply loop with a mid-loop promotion, so it lives here +//! instead of inside `__rt_mixed_numeric_common`. The integer case simply reuses +//! `__rt_int_pow_checked`, which already returns a boxed Mixed cell. +//! - The integer path is taken only when BOTH payload tags are exactly `0` (integer). +//! Every other combination — a double payload, a numeric string, a bool, null — falls +//! through to `pow((double) l, (double) r)`, which is what this operator did for all +//! Mixed operands before the integer path existed. Narrowing the fast path this way is +//! what keeps `"2.5" ** 2` a float while making `$i ** $j` an int. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits `__rt_mixed_numeric_pow` for both AArch64 and x86_64. +/// +/// Input: AArch64 x0 = left Mixed*, x1 = right Mixed* +/// x86_64 rax = left Mixed*, rdi = right Mixed* +/// Output: boxed Mixed pointer in the integer result register (x0 / rax), holding an +/// integer when both operands were integers and PHP keeps an int, a double otherwise. +pub fn emit_mixed_numeric_pow(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mixed_numeric_pow_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: mixed_numeric_pow ---"); + emitter.label_global("__rt_mixed_numeric_pow"); + + emitter.instruction("sub sp, sp, #64"); // allocate slots for both boxed operands and the saved left value + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed left operand pointer + emitter.instruction("str x1, [sp, #8]"); // save the boxed right operand pointer + + // -- integer exponentiation only when both payloads are genuine integers -- + emitter.instruction("bl __rt_mixed_unbox"); // inspect the left boxed payload tag + emitter.instruction("cmp x0, #0"); // runtime tag 0 = integer + emitter.instruction("b.ne __rt_mixed_numeric_pow_float"); // any other payload uses the double path + emitter.instruction("ldr x0, [sp, #8]"); // load the boxed right operand pointer + emitter.instruction("bl __rt_mixed_unbox"); // inspect the right boxed payload tag + emitter.instruction("cmp x0, #0"); // runtime tag 0 = integer + emitter.instruction("b.ne __rt_mixed_numeric_pow_float"); // any other payload uses the double path + + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed left operand before casting to integer + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the base to an integer + emitter.instruction("str x0, [sp, #16]"); // save the integer base across the exponent cast + emitter.instruction("ldr x0, [sp, #8]"); // reload the boxed right operand before casting to integer + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the exponent to an integer + emitter.instruction("mov x1, x0"); // place the exponent in the second helper argument + emitter.instruction("ldr x0, [sp, #16]"); // place the base in the first helper argument + emitter.instruction("bl __rt_int_pow_checked"); // php-src int ** int, already boxed as a Mixed cell + emitter.instruction("b __rt_mixed_numeric_pow_done"); // restore the helper frame and return + + // -- double path: pow((double) left, (double) right) -- + emitter.label("__rt_mixed_numeric_pow_float"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed left operand before casting to double + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the base to a double + emitter.instruction("str d0, [sp, #24]"); // save the base across the exponent cast + emitter.instruction("ldr x0, [sp, #8]"); // reload the boxed right operand before casting to double + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the exponent to a double + emitter.instruction("fmov d1, d0"); // place the exponent in the second libc pow argument + emitter.instruction("ldr d0, [sp, #24]"); // place the base in the first libc pow argument + emitter.bl_c("pow"); // pow(base, exponent) + emitter.instruction("fmov x1, d0"); // move the double bits into the Mixed helper payload register + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the double result into a Mixed cell + + emitter.label("__rt_mixed_numeric_pow_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the helper stack frame + emitter.instruction("ret"); // return to generated code with boxed Mixed result in x0 +} + +/// Emits the Linux x86_64 variant of `__rt_mixed_numeric_pow`. +/// +/// Mirrors the AArch64 helper with the mixed-helper x86_64 convention: `rax` = left +/// Mixed*, `rdi` = right Mixed*, boxed Mixed pointer returned in `rax`. +fn emit_mixed_numeric_pow_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_numeric_pow ---"); + emitter.label_global("__rt_mixed_numeric_pow"); + + emitter.instruction("push rbp"); // save the caller frame pointer before nested runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // allocate aligned slots for both boxed operands and the saved base + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the boxed left operand pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the boxed right operand pointer + + emitter.instruction("call __rt_mixed_unbox"); // inspect the left boxed payload tag + emitter.instruction("cmp rax, 0"); // runtime tag 0 = integer + emitter.instruction("jne __rt_mixed_numeric_pow_float_x"); // any other payload uses the double path + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // load the boxed right operand pointer + emitter.instruction("call __rt_mixed_unbox"); // inspect the right boxed payload tag + emitter.instruction("cmp rax, 0"); // runtime tag 0 = integer + emitter.instruction("jne __rt_mixed_numeric_pow_float_x"); // any other payload uses the double path + + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed left operand before casting to integer + emitter.instruction("call __rt_mixed_cast_int"); // coerce the base to an integer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the integer base across the exponent cast + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the boxed right operand before casting to integer + emitter.instruction("call __rt_mixed_cast_int"); // coerce the exponent to an integer + emitter.instruction("mov rsi, rax"); // place the exponent in the second helper argument + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // place the base in the first helper argument + emitter.instruction("call __rt_int_pow_checked"); // php-src int ** int, already boxed as a Mixed cell + emitter.instruction("jmp __rt_mixed_numeric_pow_done_x"); // restore the helper frame and return + + emitter.label("__rt_mixed_numeric_pow_float_x"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed left operand before casting to double + emitter.instruction("call __rt_mixed_cast_float"); // coerce the base to a double + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the base across the exponent cast + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the boxed right operand before casting to double + emitter.instruction("call __rt_mixed_cast_float"); // coerce the exponent to a double + emitter.instruction("movapd xmm1, xmm0"); // place the exponent in the second libc pow argument + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 32]"); // place the base in the first libc pow argument + emitter.bl_c("pow"); // pow(base, exponent) + emitter.instruction("movq rdi, xmm0"); // move the double bits into the Mixed helper payload register + emitter.instruction("xor rsi, rsi"); // double payloads do not use a high word + emitter.instruction("mov rax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the double result into a Mixed cell + + emitter.label("__rt_mixed_numeric_pow_done_x"); + emitter.instruction("add rsp, 64"); // release the helper stack frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to generated code with boxed Mixed result in rax +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies both targets emit the integer fast path (delegating to + /// `__rt_int_pow_checked`) and the `pow()` fallback for every non-integer payload. + #[test] + fn test_emit_mixed_numeric_pow_has_int_and_float_paths() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_mixed_numeric_pow(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_mixed_numeric_pow:\n"), "missing entry point for {:?}", arch); + assert!(asm.contains("__rt_int_pow_checked"), "missing integer path for {:?}", arch); + assert!(asm.contains("__rt_mixed_cast_float"), "missing double path for {:?}", arch); + assert!(asm.contains("__rt_mixed_from_value"), "missing boxing call for {:?}", arch); + } + } +} diff --git a/src/codegen_support/runtime/arrays/mod.rs b/src/codegen_support/runtime/arrays/mod.rs index eb114ef664..3125af04d6 100644 --- a/src/codegen_support/runtime/arrays/mod.rs +++ b/src/codegen_support/runtime/arrays/mod.rs @@ -10,6 +10,8 @@ mod array_chunk; mod array_chunk_refcounted; +mod array_chunk_to_hash; +mod array_count_values; mod array_column; mod array_column_mixed; mod array_column_ref; @@ -37,6 +39,7 @@ mod array_free_deep; mod array_get_mixed_key; mod array_grow; mod array_hash_union; +mod array_internal_pointer; mod array_intersect; mod array_intersect_refcounted; mod array_intersect_key; @@ -68,6 +71,7 @@ mod array_rand; mod random_u32; mod random_uniform; mod array_reduce; +mod array_reduce_str; mod array_replace; mod array_replace_recursive; mod array_reverse; @@ -76,12 +80,16 @@ mod array_search; mod array_shift; mod array_slice; mod array_slice_refcounted; +mod array_slice_to_hash; mod array_splice; +mod array_splice_insert; mod array_splice_refcounted; +mod array_splice_str; mod array_strict_eq; mod array_sum; mod array_sum_mixed; mod array_to_hash; +mod array_to_hash_reverse; mod array_to_mixed; mod array_udiff_uintersect; mod array_union; @@ -120,6 +128,7 @@ mod hash_map; mod hash_iter; mod hash_new; mod hash_set; +mod hash_sort; mod hash_spread; mod hash_sum_mixed; mod hash_to_mixed; @@ -133,7 +142,7 @@ mod heap_debug_validate_free_list; mod heap_kind; mod heap_free; mod in_array_mixed_int; -mod ksort; +mod min_max_container; mod natsort; mod object_free_deep; mod range; @@ -149,28 +158,36 @@ mod mixed_instanceof; mod mixed_cast_bool; mod mixed_cast_float; mod mixed_cast_int; +mod mixed_intval_base; mod mixed_cast_string; mod mixed_free_deep; mod mixed_count; mod mixed_is_empty; mod mixed_numeric_binops; mod int_checked_binops; +mod int_pow_checked; +mod mixed_numeric_pow; mod mixed_strict_eq; mod mixed_unbox; mod mixed_write_stdout; mod refcount; mod shuffle; +mod slice_bounds; mod sort_int; mod sort_str; mod undefined_array_key_warning; mod usort; +mod usort_str; pub(super) mod value_error; pub use array_chunk::emit_array_chunk; /// Emit array chunk helper (split array into chunks). pub use array_chunk_refcounted::emit_array_chunk_refcounted; /// Emit refcounted array chunk helper. +pub use array_chunk_to_hash::emit_array_chunk_to_hash; +/// Emit key-preserving array chunk helper (array_chunk preserve_keys). pub use array_column::emit_array_column; +pub use array_count_values::{emit_array_count_values, ARRAY_COUNT_VALUES_SKIPPED_MESSAGES}; /// Emit array column extraction helper. pub use array_column_mixed::emit_array_column_mixed; /// Emit Mixed-type array column helper. @@ -192,6 +209,12 @@ pub use array_diff_key::emit_array_diff_key; /// Emit array difference by key helper. pub use array_edge_key::emit_array_edge_key; /// Emit array first/last key helper (array_key_first / array_key_last). +pub use array_internal_pointer::emit_array_ptr_key; +/// Emit the internal-array-pointer key boxing helper (key()). +pub use array_internal_pointer::emit_array_ptr_seek; +/// Emit the internal-array-pointer seek helper (reset/end/next/prev). +pub use array_internal_pointer::emit_array_ptr_value; +/// Emit the internal-array-pointer value boxing helper (current() and friends). pub use array_ensure_unique::emit_array_ensure_unique; /// Emit array uniqueness enforcement helper. pub use array_fill::emit_array_fill; @@ -286,6 +309,8 @@ pub use random_u32::emit_random_u32; pub use random_uniform::emit_random_uniform; /// Emit uniform random integer helper. pub use array_reduce::emit_array_reduce; +/// Emit string-array reduce helper. +pub use array_reduce_str::emit_array_reduce_str; /// Emit array reduce helper. pub use array_replace::emit_array_replace; /// Emit array replace helper (right-wins hash merge). @@ -303,9 +328,16 @@ pub use array_slice::emit_array_slice; /// Emit array slice extraction helper. pub use array_slice_refcounted::emit_array_slice_refcounted; /// Emit refcounted array slice helper. +pub use array_slice_to_hash::emit_array_slice_to_hash; +/// Emit key-preserving array slice helper (array_slice preserve_keys). pub use array_splice::emit_array_splice; +pub use array_splice_insert::{ + emit_array_splice_insert, emit_array_splice_insert_boxed, + emit_array_splice_insert_refcounted, emit_array_splice_insert_unboxed, +}; /// Emit array splice helper. pub use array_splice_refcounted::emit_array_splice_refcounted; +pub use array_splice_str::{emit_array_splice_insert_str, emit_array_splice_str}; /// Emit deep array strict-equality (`===`) helper. pub use array_strict_eq::emit_array_strict_eq; /// Emit refcounted array splice helper. @@ -315,6 +347,8 @@ pub use array_sum_mixed::emit_array_sum_mixed; /// Emit boxed-Mixed array sum helper. pub use array_to_hash::emit_array_to_hash; /// Emit indexed-array-to-hash converter helper (shared by hash-based set ops). +pub use array_to_hash_reverse::emit_array_to_hash_reverse; +/// Emit key-preserving reversed indexed-array-to-hash converter helper (array_reverse preserve_keys). pub use array_to_mixed::emit_array_to_mixed; /// Emit array-to-Mixed conversion helper. pub use array_udiff_uintersect::emit_array_udiff_uintersect; @@ -379,6 +413,8 @@ pub use hash_new::emit_hash_new; /// Emit new hash helper. pub use hash_set::emit_hash_set; /// Emit hash set helper. +pub use hash_sort::emit_hash_sort; +/// Emit the hash key/value insertion-order sort helpers. pub use hash_spread::emit_hash_spread; /// Emit hash spread (array-literal flatten) helper. pub use hash_sum_mixed::emit_hash_sum_mixed; @@ -419,10 +455,10 @@ pub use iterable_unsupported_kind::emit_iterable_unsupported_kind; /// Emit unsupported iterable kind error helper. pub use iterable_write_stdout::emit_iterable_write_stdout; /// Emit iterable write to stdout helper. -pub use ksort::emit_ksort; -/// Emit key sort helper. pub use natsort::emit_natsort; /// Emit natural sort helper. +pub use min_max_container::{emit_min_max_hash, emit_min_max_mixed, emit_min_max_str}; +/// Emit the single-array `min()` / `max()` reductions for Mixed, string, and hash containers. pub use mixed_abs::emit_mixed_abs; /// Emit a resource-aware owned Mixed value read. pub use mixed_clone::emit_mixed_clone; @@ -435,6 +471,7 @@ pub use mixed_cast_bool::emit_mixed_cast_bool; pub use mixed_cast_float::emit_mixed_cast_float; /// Emit Mixed-to-float cast helper. pub use mixed_cast_int::emit_mixed_cast_int; +pub use mixed_intval_base::emit_mixed_intval_base; /// Emit Mixed-to-integer cast helper. pub use mixed_cast_string::emit_mixed_cast_string; /// Emit Mixed-to-string cast helper. @@ -447,6 +484,8 @@ pub use mixed_is_empty::emit_mixed_is_empty; pub use mixed_numeric_binops::emit_mixed_numeric_binops; /// Emit Mixed numeric binary operations helper. pub use int_checked_binops::emit_int_checked_binops; +pub use int_pow_checked::emit_int_pow_checked; +pub use mixed_numeric_pow::emit_mixed_numeric_pow; /// Emit checked integer add/sub/mul helpers with overflow-to-float promotion. pub use mixed_strict_eq::emit_mixed_strict_eq; /// Emit Mixed strict equality check helper. @@ -469,3 +508,5 @@ pub use sort_str::emit_sort_str; pub use undefined_array_key_warning::emit_undefined_array_key_warning; /// Emit user-defined sort helper. pub use usort::emit_usort; +/// Emit user-defined string-array sort helper. +pub use usort_str::emit_usort_str; diff --git a/src/codegen_support/runtime/arrays/range.rs b/src/codegen_support/runtime/arrays/range.rs index a32e2625ec..f628113be4 100644 --- a/src/codegen_support/runtime/arrays/range.rs +++ b/src/codegen_support/runtime/arrays/range.rs @@ -7,9 +7,18 @@ //! //! Key details: //! - Range allocation must size the output array before filling so capacity and heap accounting stay consistent. +//! - The inclusive element count `|end - start| / |step| + 1` is computed in signed 64-bit arithmetic +//! and can overflow for wide intervals. A real count is always >= 1, so a computed count <= 0 means +//! the interval wrapped and the range is rejected instead of allocating a mis-sized array. +//! - `__rt_range` takes PHP's `$step` as a THIRD argument (`x2` / `rdx`). Its sign is ignored — the +//! traversal direction comes from `start` vs `end`, exactly like php-src — and the caller is +//! responsible for raising PHP's `ValueError`s for a zero step, a negative step on an increasing +//! range, and a step wider than the spanned interval before calling in. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::data::RANGE_SIZE_MSG; /// Dispatches to the architecture-specific range-emitter implementation. pub fn emit_range(emitter: &mut Emitter) { @@ -29,24 +38,37 @@ pub fn emit_range(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save start value emitter.instruction("str x1, [sp, #8]"); // save end value - // -- determine direction and calculate count -- + // -- normalize the requested step to its traversal magnitude -- + emitter.instruction("cmp x2, #0"); // is the requested PHP step negative? + emitter.instruction("cneg x9, x2, lt"); // x9 = |step|, the magnitude every direction walks by + emitter.instruction("cmp x9, #0"); // a zero or unrepresentable magnitude cannot advance the range + emitter.instruction("b.le __rt_range_size_fail"); // reject it instead of dividing by zero below + + // -- determine direction and calculate the spanned interval -- emitter.instruction("cmp x0, x1"); // compare start with end emitter.instruction("b.gt __rt_range_descending"); // if start > end, use descending path - // -- ascending: count = end - start + 1 -- - emitter.instruction("sub x2, x1, x0"); // x2 = end - start - emitter.instruction("add x2, x2, #1"); // x2 = count = end - start + 1 - emitter.instruction("mov x7, #1"); // x7 = step = +1 (ascending) - emitter.instruction("b __rt_range_alloc"); // jump to allocation + // -- ascending: span = end - start, traversal step = +|step| -- + emitter.instruction("sub x2, x1, x0"); // x2 = span = end - start + emitter.instruction("mov x7, x9"); // x7 = step = +|step| (ascending) + emitter.instruction("b __rt_range_count"); // jump to the shared element-count computation - // -- descending: count = start - end + 1 -- + // -- descending: span = start - end, traversal step = -|step| -- emitter.label("__rt_range_descending"); - emitter.instruction("sub x2, x0, x1"); // x2 = start - end - emitter.instruction("add x2, x2, #1"); // x2 = count = start - end + 1 - emitter.instruction("mov x7, #-1"); // x7 = step = -1 (descending) + emitter.instruction("sub x2, x0, x1"); // x2 = span = start - end + emitter.instruction("neg x7, x9"); // x7 = step = -|step| (descending) + + // -- count = span / |step| + 1 -- + emitter.label("__rt_range_count"); + emitter.instruction("cmp x2, #0"); // an inclusive span is never negative + emitter.instruction("b.lt __rt_range_size_fail"); // a negative span means the interval overflowed + emitter.instruction("udiv x2, x2, x9"); // x2 = whole steps that fit inside the span + emitter.instruction("add x2, x2, #1"); // x2 = count, the inclusive element count // -- allocate array -- emitter.label("__rt_range_alloc"); + emitter.instruction("cmp x2, #0"); // an inclusive range always holds at least one element + emitter.instruction("b.le __rt_range_size_fail"); // a non-positive count means the interval overflowed emitter.instruction("str x2, [sp, #16]"); // save count emitter.instruction("str x7, [sp, #8]"); // save step (reuse end slot, no longer needed) emitter.instruction("mov x0, x2"); // x0 = capacity = count @@ -54,18 +76,18 @@ pub fn emit_range(emitter: &mut Emitter) { emitter.instruction("bl __rt_array_new"); // allocate new array emitter.instruction("str x0, [sp, #24]"); // save new array pointer - // -- fill array with values from start, stepping by +1 or -1 -- + // -- fill array with values from start, stepping by the signed traversal step -- emitter.instruction("add x3, x0, #24"); // x3 = data base of new array emitter.instruction("ldr x4, [sp, #0]"); // x4 = current value = start emitter.instruction("ldr x5, [sp, #16]"); // x5 = count - emitter.instruction("ldr x7, [sp, #8]"); // x7 = step (+1 or -1) + emitter.instruction("ldr x7, [sp, #8]"); // x7 = signed traversal step emitter.instruction("mov x6, #0"); // x6 = i = 0 emitter.label("__rt_range_loop"); emitter.instruction("cmp x6, x5"); // compare i with count emitter.instruction("b.ge __rt_range_done"); // if i >= count, filling complete emitter.instruction("str x4, [x3, x6, lsl #3]"); // data[i] = current value - emitter.instruction("add x4, x4, x7"); // current value += step (+1 or -1) + emitter.instruction("add x4, x4, x7"); // current value += the signed traversal step emitter.instruction("add x6, x6, #1"); // i += 1 emitter.instruction("b __rt_range_loop"); // continue loop @@ -79,12 +101,21 @@ pub fn emit_range(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #48"); // deallocate stack frame emitter.instruction("ret"); // return with x0 = array [start..end] + + // -- fatal error: the inclusive range does not fit in an array -- + emitter.label("__rt_range_size_fail"); + emitter.instruction("mov x0, #2"); // fd = stderr + abi::emit_symbol_address(emitter, "x1", "_range_size_err_msg"); + emitter.instruction(&format!("mov x2, #{}", RANGE_SIZE_MSG.len())); // pass the exact range-size diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 + emitter.syscall(1); } /// Emits the x86_64 Linux implementation of `__rt_range` for both ascending and descending integer ranges. -/// Input: rdi = start (inclusive), rsi = end (inclusive) +/// Input: rdi = start (inclusive), rsi = end (inclusive), rdx = step (sign ignored, magnitude used) /// Output: rax = pointer to new indexed array containing values from start to end -/// Uses rbp-based frame with spill slots for start, end, count, step, and array pointer. +/// Uses rbp-based frame with spill slots for start, end, count, traversal step, and array pointer. /// Preserves 16-byte stack alignment for nested calls. fn emit_range_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); @@ -96,35 +127,49 @@ fn emit_range_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 40"); // reserve aligned spill slots for range-construction bookkeeping while keeping nested calls 16-byte aligned emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the inclusive range start value across count calculation and destination-array allocation emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the inclusive range end value across count calculation and destination-array allocation + emitter.instruction("mov r10, rdx"); // copy the requested PHP step before normalizing it to a traversal magnitude + emitter.instruction("mov r11, r10"); // stage the negated requested step for the conditional magnitude select + emitter.instruction("neg r11"); // negate the requested step so a negative one yields its magnitude + emitter.instruction("test r10, r10"); // is the requested PHP step negative? + emitter.instruction("cmovs r10, r11"); // r10 = |step|, the magnitude every direction walks by + emitter.instruction("cmp r10, 0"); // a zero or unrepresentable magnitude cannot advance the range + emitter.instruction("jle __rt_range_size_fail"); // reject it instead of dividing by zero below emitter.instruction("cmp rdi, rsi"); // compare the inclusive range start and end values to choose the traversal direction emitter.instruction("jg __rt_range_descending_x86"); // switch to the descending range path when the start value is greater than the end value - emitter.instruction("mov rax, rsi"); // copy the inclusive range end value before subtracting the start value to derive the element count + emitter.instruction("mov rax, rsi"); // copy the inclusive range end value before subtracting the start value to derive the spanned interval emitter.instruction("sub rax, rdi"); // compute end - start for the ascending integer range - emitter.instruction("add rax, 1"); // convert the inclusive ascending difference into the final element count - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the computed ascending element count across destination-array allocation - emitter.instruction("mov QWORD PTR [rbp - 32], 1"); // preserve the ascending traversal step so the fill loop can advance by +1 - emitter.instruction("jmp __rt_range_alloc_x86"); // jump to the shared destination-array allocation path after preparing the ascending count and step + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // preserve the ascending traversal step so the fill loop can advance by +|step| + emitter.instruction("jmp __rt_range_count_x86"); // jump to the shared element-count computation after preparing the ascending span and step emitter.label("__rt_range_descending_x86"); - emitter.instruction("mov rax, rdi"); // copy the inclusive range start value before subtracting the end value to derive the element count + emitter.instruction("mov rax, rdi"); // copy the inclusive range start value before subtracting the end value to derive the spanned interval emitter.instruction("sub rax, rsi"); // compute start - end for the descending integer range - emitter.instruction("add rax, 1"); // convert the inclusive descending difference into the final element count - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the computed descending element count across destination-array allocation - emitter.instruction("mov QWORD PTR [rbp - 32], -1"); // preserve the descending traversal step so the fill loop can advance by -1 + emitter.instruction("mov r11, r10"); // stage the traversal magnitude before negating it for the descending direction + emitter.instruction("neg r11"); // negate the traversal magnitude so the fill loop walks downwards + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // preserve the descending traversal step so the fill loop can advance by -|step| + emitter.label("__rt_range_count_x86"); + emitter.instruction("cmp rax, 0"); // an inclusive span is never negative + emitter.instruction("jl __rt_range_size_fail"); // a negative span means the interval overflowed + emitter.instruction("xor edx, edx"); // clear the high dividend word before the unsigned span division + emitter.instruction("div r10"); // rax = whole steps that fit inside the span + emitter.instruction("add rax, 1"); // convert the whole-step count into the inclusive element count + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the computed element count across destination-array allocation emitter.label("__rt_range_alloc_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the final integer range length as the destination indexed-array capacity to the constructor + emitter.instruction("cmp rdi, 0"); // an inclusive range always holds at least one element + emitter.instruction("jle __rt_range_size_fail"); // a non-positive count means the interval overflowed emitter.instruction("mov rsi, 8"); // use 8-byte payload slots because the range helper produces an indexed array of integers emitter.instruction("call __rt_array_new"); // allocate the destination integer range array through the shared x86_64 indexed-array constructor emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // preserve the destination integer range array pointer while the fill loop writes payload slots emitter.instruction("lea r8, [rax + 24]"); // compute the destination integer range payload base address once before entering the fill loop emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the current integer value from the inclusive range start before entering the fill loop emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the final integer range element count before entering the fill loop - emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the traversal step before entering the fill loop + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the signed traversal step before entering the fill loop emitter.instruction("xor rcx, rcx"); // initialize the range fill loop index to the first destination payload slot emitter.label("__rt_range_loop_x86"); emitter.instruction("cmp rcx, r10"); // compare the current range fill loop index against the final element count emitter.instruction("jge __rt_range_done_x86"); // stop once every destination integer payload slot has been initialized emitter.instruction("mov QWORD PTR [r8 + rcx * 8], r9"); // store the current integer value into the selected destination range payload slot - emitter.instruction("add r9, r11"); // advance the current integer value by the preserved traversal step for the next payload slot + emitter.instruction("add r9, r11"); // advance the current integer value by the preserved signed traversal step for the next payload slot emitter.instruction("add rcx, 1"); // advance the range fill loop index after initializing one destination payload slot emitter.instruction("jmp __rt_range_loop_x86"); // continue filling integer range payload slots until the inclusive interval is exhausted emitter.label("__rt_range_done_x86"); @@ -134,4 +179,15 @@ fn emit_range_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 40"); // release the range-construction spill slots before returning emitter.instruction("pop rbp"); // restore the caller frame pointer before returning emitter.instruction("ret"); // return the constructed integer range array pointer in rax + + // -- fatal error: the inclusive range does not fit in an array -- + emitter.label("__rt_range_size_fail"); + emitter.instruction("mov edi, 2"); // fd = stderr for the range-size fatal error message + abi::emit_symbol_address(emitter, "rsi", "_range_size_err_msg"); + emitter.instruction(&format!("mov edx, {}", RANGE_SIZE_MSG.len())); // pass the exact range-size diagnostic byte count + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // print the fatal range-size message to stderr + emitter.instruction("mov edi, 1"); // exit code 1 for an unrepresentable range size + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the range-size failure } diff --git a/src/codegen_support/runtime/arrays/slice_bounds.rs b/src/codegen_support/runtime/arrays/slice_bounds.rs new file mode 100644 index 0000000000..917a46a987 --- /dev/null +++ b/src/codegen_support/runtime/arrays/slice_bounds.rs @@ -0,0 +1,122 @@ +//! Purpose: +//! Emits the shared PHP `$offset`/`$length` normalization prologue used by every slice-like +//! indexed-array runtime helper (`__rt_array_slice`, `__rt_array_slice_refcounted`, +//! `__rt_array_splice`, `__rt_array_splice_refcounted`). +//! +//! Called from: +//! - `crate::codegen_support::runtime::arrays::array_slice`, +//! `crate::codegen_support::runtime::arrays::array_slice_refcounted`, +//! `crate::codegen_support::runtime::arrays::array_splice` and +//! `crate::codegen_support::runtime::arrays::array_splice_refcounted`. +//! +//! Key details: +//! - There is no out-of-band `i64` a PHP `$length` cannot take, so "no `$length` given" travels in a +//! dedicated fourth argument register instead of a magic length value. `-1` used to double as the +//! until-the-end sentinel, which collided with PHP's `-1` = "stop one element before the end". +//! - The emitted sequence is the single source of truth for PHP's slice window arithmetic, so the +//! scalar and refcounted slice/splice helpers cannot drift apart. +//! - Every clamp is signed and the result window is always inside `[0, length]`, so no caller can +//! publish a negative logical length or copy outside the source payload. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits PHP's `array_slice`/`array_splice` `$offset`/`$length` normalization. +/// +/// `prefix` seeds the local label names, so each helper that inlines this sequence must pass its own +/// unique string. +/// +/// # ABI +/// - **ARM64** — in: `x0` = source indexed-array pointer, `x1` = raw `$offset`, `x2` = raw `$length`, +/// `x3` = 1 when the caller passed a `$length` and 0 when it was omitted or `null`. +/// Out: `x1` = normalized offset in `[0, n]`, `x2` = clamped window length in `[0, n - offset]`, +/// `x9` = source length `n`, `x10` = elements available from the normalized offset. Clobbers +/// `x9`/`x10`; `x0` is preserved. +/// - **x86_64** — in: `rdi`, `rsi`, `rdx`, `rcx` with the same meaning. +/// Out: `rsi`/`rdx` normalized as above, `r10` = `n`, `r11` = available elements. Clobbers +/// `r10`/`r11`; `rdi` is preserved. +/// +/// # Semantics +/// - A negative `$offset` counts backwards from the end and clamps to the start; a too-large +/// `$offset` clamps to the end, which yields an empty window instead of a negative one. +/// - An omitted `$length` takes every remaining element. +/// - A negative `$length` stops that many elements before the end of the source, clamped to an empty +/// window when it would run past the normalized offset. +/// - A positive `$length` is clamped to the elements actually available. +/// +/// Neither `n + $offset` nor `available + $length` can overflow: both add a non-negative value to a +/// negative one, and signed overflow needs matching signs. +pub fn emit_slice_bounds(emitter: &mut Emitter, prefix: &str) { + if emitter.target.arch == Arch::X86_64 { + emit_slice_bounds_x86_64(emitter, prefix); + return; + } + + emitter.comment("-- normalize the PHP slice window: offset, then length --"); + emitter.instruction("ldr x9, [x0]"); // x9 = source indexed-array logical length + emitter.instruction("cmp x1, #0"); // does the caller count the offset backwards from the end? + emitter.instruction(&format!("b.ge {}_off_fwd", prefix)); // forward offsets only need the upper clamp + emitter.instruction("add x1, x9, x1"); // offset = length + offset for backward offsets + emitter.instruction("cmp x1, #0"); // did the backward offset run past the start of the source? + emitter.instruction("csel x1, xzr, x1, lt"); // clamp a too-far backward offset to the first element + emitter.instruction(&format!("b {}_off_ready", prefix)); // the backward offset is now inside the source bounds + emitter.label(&format!("{}_off_fwd", prefix)); + emitter.instruction("cmp x1, x9"); // does the forward offset start past the end of the source? + emitter.instruction("csel x1, x9, x1, gt"); // clamp a too-large offset to the end so the window stays empty + emitter.label(&format!("{}_off_ready", prefix)); + emitter.instruction("sub x10, x9, x1"); // x10 = elements available from the normalized offset + emitter.instruction(&format!("cbz x3, {}_len_absent", prefix)); // an omitted length takes every remaining element + emitter.instruction("cmp x2, #0"); // does the caller stop a number of elements before the end? + emitter.instruction(&format!("b.lt {}_len_back", prefix)); // negative lengths are counted back from the source end + emitter.instruction("cmp x2, x10"); // does the requested length run past the available elements? + emitter.instruction("csel x2, x10, x2, gt"); // clamp the requested length to the available elements + emitter.instruction(&format!("b {}_len_ready", prefix)); // the requested length now fits the source window + emitter.label(&format!("{}_len_back", prefix)); + emitter.instruction("add x2, x2, x10"); // length = available + negative length (operands differ in sign, cannot overflow) + emitter.instruction("cmp x2, #0"); // did the backward length consume more than the window holds? + emitter.instruction("csel x2, xzr, x2, lt"); // an over-large backward length yields an empty window, never a negative one + emitter.instruction(&format!("b {}_len_ready", prefix)); // the backward length is now a non-negative element count + emitter.label(&format!("{}_len_absent", prefix)); + emitter.instruction("mov x2, x10"); // no length given: take every element from the normalized offset + emitter.label(&format!("{}_len_ready", prefix)); +} + +/// Emits the x86_64 variant of the shared slice-window normalization. +/// +/// Mirrors the ARM64 sequence instruction for instruction; only the System V register names and the +/// branch-based clamps differ. See [`emit_slice_bounds`] for the full ABI and semantics. +fn emit_slice_bounds_x86_64(emitter: &mut Emitter, prefix: &str) { + emitter.comment("-- normalize the PHP slice window: offset, then length --"); + emitter.instruction("mov r10, QWORD PTR [rdi]"); // r10 = source indexed-array logical length + emitter.instruction("cmp rsi, 0"); // does the caller count the offset backwards from the end? + emitter.instruction(&format!("jge {}_off_fwd_x86", prefix)); // forward offsets only need the upper clamp + emitter.instruction("add rsi, r10"); // offset = length + offset for backward offsets + emitter.instruction("cmp rsi, 0"); // did the backward offset run past the start of the source? + emitter.instruction(&format!("jge {}_off_ready_x86", prefix)); // keep the backward offset once it is inside the source bounds + emitter.instruction("xor esi, esi"); // clamp a too-far backward offset to the first element + emitter.instruction(&format!("jmp {}_off_ready_x86", prefix)); // the backward offset is now inside the source bounds + emitter.label(&format!("{}_off_fwd_x86", prefix)); + emitter.instruction("cmp rsi, r10"); // does the forward offset start past the end of the source? + emitter.instruction(&format!("jle {}_off_ready_x86", prefix)); // keep the forward offset when it still points inside the source + emitter.instruction("mov rsi, r10"); // clamp a too-large offset to the end so the window stays empty + emitter.label(&format!("{}_off_ready_x86", prefix)); + emitter.instruction("mov r11, r10"); // seed the availability scratch register from the source length + emitter.instruction("sub r11, rsi"); // r11 = elements available from the normalized offset + emitter.instruction("test rcx, rcx"); // did the caller pass an explicit length at all? + emitter.instruction(&format!("je {}_len_absent_x86", prefix)); // an omitted length takes every remaining element + emitter.instruction("cmp rdx, 0"); // does the caller stop a number of elements before the end? + emitter.instruction(&format!("jl {}_len_back_x86", prefix)); // negative lengths are counted back from the source end + emitter.instruction("cmp rdx, r11"); // does the requested length run past the available elements? + emitter.instruction(&format!("jle {}_len_ready_x86", prefix)); // keep the requested length when it fits the available elements + emitter.instruction("mov rdx, r11"); // clamp the requested length to the available elements + emitter.instruction(&format!("jmp {}_len_ready_x86", prefix)); // the requested length now fits the source window + emitter.label(&format!("{}_len_back_x86", prefix)); + emitter.instruction("add rdx, r11"); // length = available + negative length (operands differ in sign, cannot overflow) + emitter.instruction("cmp rdx, 0"); // did the backward length consume more than the window holds? + emitter.instruction(&format!("jge {}_len_ready_x86", prefix)); // keep a backward length that still selects at least one element + emitter.instruction("xor edx, edx"); // an over-large backward length yields an empty window, never a negative one + emitter.instruction(&format!("jmp {}_len_ready_x86", prefix)); // the backward length is now a non-negative element count + emitter.label(&format!("{}_len_absent_x86", prefix)); + emitter.instruction("mov rdx, r11"); // no length given: take every element from the normalized offset + emitter.label(&format!("{}_len_ready_x86", prefix)); +} diff --git a/src/codegen_support/runtime/arrays/usort_str.rs b/src/codegen_support/runtime/arrays/usort_str.rs new file mode 100644 index 0000000000..322e035fc5 --- /dev/null +++ b/src/codegen_support/runtime/arrays/usort_str.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! Emits the `__rt_usort_str` runtime helper assembly used by `usort()` when the +//! receiver is an indexed string array. String arrays store 16-byte +//! `[ptr:8][len:8]` payload slots, so the 8-byte slot permuter `__rt_usort` cannot +//! reorder them without corrupting the descriptors. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::arrays`. +//! +//! Key details: +//! - The algorithm is a stable insertion sort over whole 16-byte slots, matching +//! PHP 8's stable `usort()` ordering for elements the comparator reports equal. +//! - The comparator ABI mirrors a PHP function of two string parameters: on +//! AArch64 `x0`/`x1` carry the left pointer/length, `x2`/`x3` the right +//! pointer/length, and `x4` the optional capture environment; on x86_64 the +//! same values land in `rdi`/`rsi`, `rdx`/`rcx`, and `r8`. The integer result is +//! read from `x0`/`rax`. +//! - Every piece of loop state lives in the frame because the comparator callback +//! is free to clobber all caller-saved registers; the helper itself touches no +//! callee-saved register other than the frame pointer. +//! - Slots are permuted in place, so string payload ownership stays with the +//! array and no refcount traffic is needed. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// usort_str: sorts an indexed string array in place through a user comparator. +/// +/// Input: AArch64 `x0` = comparator address, `x1` = array pointer, `x2` = optional +/// capture environment pointer (0 when the comparator takes no environment); +/// x86_64 `rdi` / `rsi` / `rdx` respectively. +/// Output: none — the array payload is reordered in place and keys are implicitly +/// renumbered because indexed arrays carry no key storage. +/// Arrays shorter than two elements return immediately. +pub fn emit_usort_str(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_usort_str_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: usort_str ---"); + emitter.label_global("__rt_usort_str"); + + // Frame (96 bytes): [0]=length [8]=base [16]=i [24]=keyptr [32]=keylen + // [40]=j [48]=comparator [56]=env [80]=x29,x30 + emitter.instruction("sub sp, sp, #96"); // reserve the insertion-sort state that must survive comparator calls + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #48]"); // save the comparator address for every inner-loop call + emitter.instruction("str x2, [sp, #56]"); // save the optional comparator capture environment pointer + emitter.instruction("ldr x9, [x1]"); // x9 = array length from the header + emitter.instruction("str x9, [sp, #0]"); // save the array length + emitter.instruction("add x9, x1, #24"); // x9 = base of the data region (skip header) + emitter.instruction("str x9, [sp, #8]"); // save the data base + emitter.instruction("mov x9, #1"); // outer-loop index i = 1 + emitter.instruction("str x9, [sp, #16]"); // save i + + emitter.label("__rt_usort_str_outer"); + emitter.instruction("ldr x3, [sp, #16]"); // reload i + emitter.instruction("ldr x1, [sp, #0]"); // reload the array length + emitter.instruction("cmp x3, x1"); // compare i with the array length + emitter.instruction("b.ge __rt_usort_str_done"); // i >= length: sorting complete + emitter.instruction("ldr x2, [sp, #8]"); // reload the data base + emitter.instruction("add x9, x2, x3, lsl #4"); // x9 = &data[i] (16-byte string slots) + emitter.instruction("ldr x4, [x9]"); // keyptr = data[i] string pointer + emitter.instruction("ldr x5, [x9, #8]"); // keylen = data[i] string length + emitter.instruction("str x4, [sp, #24]"); // save keyptr across comparator calls + emitter.instruction("str x5, [sp, #32]"); // save keylen across comparator calls + emitter.instruction("sub x6, x3, #1"); // j = i - 1 (scan the sorted prefix) + emitter.instruction("str x6, [sp, #40]"); // save j + + emitter.label("__rt_usort_str_inner"); + emitter.instruction("ldr x6, [sp, #40]"); // reload j + emitter.instruction("cmp x6, #0"); // is j below the start of the array? + emitter.instruction("b.lt __rt_usort_str_insert"); // insertion point reached + emitter.instruction("ldr x2, [sp, #8]"); // reload the data base + emitter.instruction("add x9, x2, x6, lsl #4"); // x9 = &data[j] + emitter.instruction("ldr x0, [x9]"); // comparator arg a: data[j] string pointer + emitter.instruction("ldr x1, [x9, #8]"); // comparator arg a: data[j] string length + emitter.instruction("ldr x2, [sp, #24]"); // comparator arg b: keyptr + emitter.instruction("ldr x3, [sp, #32]"); // comparator arg b: keylen + emitter.instruction("ldr x4, [sp, #56]"); // pass the capture environment after the compared string pair + emitter.instruction("ldr x9, [sp, #48]"); // reload the comparator address + emitter.instruction("blr x9"); // x0 = comparator(data[j], key) + emitter.instruction("cmp x0, #0"); // is data[j] already ordered at or before the key? + emitter.instruction("b.le __rt_usort_str_insert"); // ordered: insert here, which keeps equal elements stable + emitter.instruction("ldr x6, [sp, #40]"); // reload j for the shift + emitter.instruction("ldr x2, [sp, #8]"); // reload the data base + emitter.instruction("add x9, x2, x6, lsl #4"); // x9 = &data[j] + emitter.instruction("ldr x10, [x9]"); // data[j] string pointer + emitter.instruction("ldr x11, [x9, #8]"); // data[j] string length + emitter.instruction("str x10, [x9, #16]"); // data[j+1] pointer = data[j] pointer + emitter.instruction("str x11, [x9, #24]"); // data[j+1] length = data[j] length + emitter.instruction("sub x6, x6, #1"); // j -= 1 (continue scanning left) + emitter.instruction("str x6, [sp, #40]"); // save j + emitter.instruction("b __rt_usort_str_inner"); // continue the inner loop + + emitter.label("__rt_usort_str_insert"); + emitter.instruction("ldr x6, [sp, #40]"); // reload j + emitter.instruction("add x12, x6, #1"); // insertion index j + 1 + emitter.instruction("ldr x2, [sp, #8]"); // reload the data base + emitter.instruction("add x9, x2, x12, lsl #4"); // x9 = &data[j+1] + emitter.instruction("ldr x10, [sp, #24]"); // reload keyptr + emitter.instruction("ldr x11, [sp, #32]"); // reload keylen + emitter.instruction("str x10, [x9]"); // data[j+1] pointer = keyptr + emitter.instruction("str x11, [x9, #8]"); // data[j+1] length = keylen + emitter.instruction("ldr x3, [sp, #16]"); // reload i + emitter.instruction("add x3, x3, #1"); // advance the outer-loop index + emitter.instruction("str x3, [sp, #16]"); // save i + emitter.instruction("b __rt_usort_str_outer"); // continue the outer loop + + emitter.label("__rt_usort_str_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the insertion-sort state frame + emitter.instruction("ret"); // return (void, string array sorted in place) +} + +/// x86_64 Linux implementation of the `__rt_usort_str` runtime helper. +/// +/// Inputs (System V): `rdi` = comparator address, `rsi` = array pointer, +/// `rdx` = optional capture environment pointer. +/// Uses the same stable insertion sort as the AArch64 path; the comparator is +/// invoked with `rdi`/`rsi` = left pointer/length, `rdx`/`rcx` = right +/// pointer/length, `r8` = environment, and returns the ordering in `rax`. +/// Emits `__rt_usort_str` as a global label. +fn emit_usort_str_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: usort_str ---"); + emitter.label_global("__rt_usort_str"); + + // Frame (rbp-relative): [-8]=length [-16]=base [-24]=i [-32]=keyptr + // [-40]=keylen [-48]=j [-56]=comparator [-64]=env + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve the insertion-sort state slots and keep rsp 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the comparator address for every inner-loop call + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the optional comparator capture environment pointer + emitter.instruction("mov r8, QWORD PTR [rsi]"); // r8 = array length from the header + emitter.instruction("mov QWORD PTR [rbp - 8], r8"); // save the array length + emitter.instruction("lea r8, [rsi + 24]"); // r8 = base of the data region (skip header) + emitter.instruction("mov QWORD PTR [rbp - 16], r8"); // save the data base + emitter.instruction("mov r8, 1"); // outer-loop index i = 1 + emitter.instruction("mov QWORD PTR [rbp - 24], r8"); // save i + + emitter.label("__rt_usort_str_outer_linux_x86_64"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload i + emitter.instruction("cmp r10, QWORD PTR [rbp - 8]"); // compare i with the array length + emitter.instruction("jge __rt_usort_str_done_linux_x86_64"); // i >= length: sorting complete + emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // reload the data base + emitter.instruction("shl r10, 4"); // i * 16 (16-byte string slots) + emitter.instruction("add r9, r10"); // r9 = &data[i] + emitter.instruction("mov rax, QWORD PTR [r9]"); // keyptr = data[i] string pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save keyptr across comparator calls + emitter.instruction("mov rax, QWORD PTR [r9 + 8]"); // keylen = data[i] string length + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save keylen across comparator calls + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unscaled outer-loop index + emitter.instruction("sub r10, 1"); // j = i - 1 (scan the sorted prefix) + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save j + + emitter.label("__rt_usort_str_inner_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload j + emitter.instruction("cmp r9, 0"); // is j below the start of the array? + emitter.instruction("jl __rt_usort_str_insert_linux_x86_64"); // insertion point reached + emitter.instruction("shl r9, 4"); // j * 16 (16-byte string slots) + emitter.instruction("add r9, QWORD PTR [rbp - 16]"); // r9 = &data[j] + emitter.instruction("mov rdi, QWORD PTR [r9]"); // comparator arg a: data[j] string pointer + emitter.instruction("mov rsi, QWORD PTR [r9 + 8]"); // comparator arg a: data[j] string length + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // comparator arg b: keyptr + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // comparator arg b: keylen + emitter.instruction("mov r8, QWORD PTR [rbp - 64]"); // pass the capture environment after the compared string pair + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the comparator address + emitter.instruction("call r11"); // rax = comparator(data[j], key) + emitter.instruction("cmp rax, 0"); // is data[j] already ordered at or before the key? + emitter.instruction("jle __rt_usort_str_insert_linux_x86_64"); // ordered: insert here, which keeps equal elements stable + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload j for the shift + emitter.instruction("shl r9, 4"); // j * 16 (16-byte string slots) + emitter.instruction("add r9, QWORD PTR [rbp - 16]"); // r9 = &data[j] + emitter.instruction("mov r10, QWORD PTR [r9]"); // data[j] string pointer + emitter.instruction("mov rax, QWORD PTR [r9 + 8]"); // data[j] string length + emitter.instruction("mov QWORD PTR [r9 + 16], r10"); // data[j+1] pointer = data[j] pointer + emitter.instruction("mov QWORD PTR [r9 + 24], rax"); // data[j+1] length = data[j] length + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the unscaled inner-loop index + emitter.instruction("sub r10, 1"); // j -= 1 (continue scanning left) + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save j + emitter.instruction("jmp __rt_usort_str_inner_linux_x86_64"); // continue the inner loop + + emitter.label("__rt_usort_str_insert_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload j + emitter.instruction("add r9, 1"); // insertion index j + 1 + emitter.instruction("shl r9, 4"); // (j + 1) * 16 + emitter.instruction("add r9, QWORD PTR [rbp - 16]"); // r9 = &data[j+1] + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload keyptr + emitter.instruction("mov QWORD PTR [r9], r10"); // data[j+1] pointer = keyptr + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload keylen + emitter.instruction("mov QWORD PTR [r9 + 8], r10"); // data[j+1] length = keylen + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload i + emitter.instruction("add r10, 1"); // advance the outer-loop index + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save i + emitter.instruction("jmp __rt_usort_str_outer_linux_x86_64"); // continue the outer loop + + emitter.label("__rt_usort_str_done_linux_x86_64"); + emitter.instruction("add rsp, 64"); // release the insertion-sort state slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return (void, string array sorted in place) +} diff --git a/src/codegen_support/runtime/buffers/buffer_new.rs b/src/codegen_support/runtime/buffers/buffer_new.rs index 67d9d8eca8..e42ff86553 100644 --- a/src/codegen_support/runtime/buffers/buffer_new.rs +++ b/src/codegen_support/runtime/buffers/buffer_new.rs @@ -7,9 +7,14 @@ //! //! Key details: //! - Buffer helpers enforce extension ownership rules, including live headers, bounds checks, and fatal paths before unsafe access. +//! - `len * stride` is validated before allocating: an unchecked product wraps to a tiny block while +//! the header still advertises the pre-overflow length, and `buffer[i]` bounds checks trust that +//! header, so every in-"bounds" index past the real block would read and write foreign memory. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::data::BUFFER_ALLOC_SIZE_MSG; /// Emits the `__rt_buffer_new` runtime helper for the current target. /// @@ -29,6 +34,10 @@ use crate::codegen_support::platform::Arch; /// - header[0..8] = logical element count (set from input x0/rdi) /// - header[8..16] = element stride in bytes (set from input x1/rsi) /// - header[16..] = zero-initialized payload region (len * stride bytes) +/// +/// Rejects negative lengths and any `len * stride + 16` that does not fit in a non-negative +/// machine word by terminating through `__rt_buffer_new_size_fail`, so the stored header length +/// always describes memory the buffer actually owns. pub fn emit_buffer_new(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_buffer_new_linux_x86_64(emitter); @@ -46,9 +55,18 @@ pub fn emit_buffer_new(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save requested logical length emitter.instruction("str x1, [sp, #8]"); // save requested element stride + // -- reject negative lengths and unrepresentable payload sizes -- + emitter.instruction("cmp x0, #0"); // is the requested logical length negative? + emitter.instruction("b.lt __rt_buffer_new_size_fail"); // reject negative buffer lengths outright + emitter.instruction("umulh x9, x0, x1"); // x9 = high 64 bits of len * stride + emitter.instruction("cbnz x9, __rt_buffer_new_size_fail"); // reject payload sizes that do not fit in one machine word + emitter.instruction("mul x9, x0, x1"); // x9 = low 64 bits of len * stride + emitter.instruction("adds x9, x9, #16"); // x9 = payload size plus the 16-byte buffer header + emitter.instruction("b.hs __rt_buffer_new_size_fail"); // reject totals that carried out of the machine word + emitter.instruction("tbnz x9, #63, __rt_buffer_new_size_fail"); // reject totals the signed heap-size check would read as negative + // -- allocate header + contiguous payload -- - emitter.instruction("mul x2, x0, x1"); // compute payload byte count = len * stride - emitter.instruction("add x0, x2, #16"); // add the 16-byte buffer header + emitter.instruction("mov x0, x9"); // x0 = validated payload byte count plus the buffer header emitter.instruction("bl __rt_heap_alloc"); // allocate the full buffer payload on the shared heap // -- initialize header fields -- @@ -74,10 +92,23 @@ pub fn emit_buffer_new(emitter: &mut Emitter) { emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #32"); // release the temporary frame emitter.instruction("ret"); // return x0 = buffer header pointer + + // -- fatal error: requested buffer length cannot be represented -- + emitter.label("__rt_buffer_new_size_fail"); + emitter.instruction("mov x0, #2"); // fd = stderr + abi::emit_symbol_address(emitter, "x1", "_buffer_alloc_size_msg"); + emitter.instruction(&format!("mov x2, #{}", BUFFER_ALLOC_SIZE_MSG.len())); // pass the exact buffer-length diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 + emitter.syscall(1); } /// Emits the `__rt_buffer_new` runtime helper for the x86_64 Linux target. /// Private; dispatcher lives in `emit_buffer_new`. +/// +/// Applies the same length validation as the ARM64 path: negative lengths and any +/// `len * stride + 16` that does not fit in a non-negative machine word terminate the process +/// through `__rt_buffer_new_size_fail`. fn emit_buffer_new_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: buffer_new ---"); @@ -90,9 +121,15 @@ fn emit_buffer_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the requested logical length across the nested heap allocation call emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the requested element stride across the nested heap allocation call + // -- reject negative lengths and unrepresentable payload sizes -- + emitter.instruction("test rax, rax"); // is the requested logical length negative? + emitter.instruction("js __rt_buffer_new_size_fail"); // reject negative buffer lengths outright + // -- allocate header + contiguous payload -- emitter.instruction("imul rax, rdi"); // compute payload byte count = len * stride in the x86_64 heap-allocation size register + emitter.instruction("jo __rt_buffer_new_size_fail"); // reject payload sizes that do not fit in one machine word emitter.instruction("add rax, 16"); // add the 16-byte buffer header before requesting the backing allocation + emitter.instruction("jo __rt_buffer_new_size_fail"); // reject totals the signed heap-size accounting would read as negative emitter.instruction("call __rt_heap_alloc"); // allocate the buffer header plus contiguous payload through the shared x86_64 heap wrapper emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the allocated buffer header pointer while materializing the header fields @@ -121,4 +158,15 @@ fn emit_buffer_new_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("add rsp, 32"); // release the temporary spill slots reserved for buffer_new emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to generated code emitter.instruction("ret"); // return rax = buffer header pointer + + // -- fatal error: requested buffer length cannot be represented -- + emitter.label("__rt_buffer_new_size_fail"); + emitter.instruction("mov edi, 2"); // fd = stderr for the buffer-length fatal error message + abi::emit_symbol_address(emitter, "rsi", "_buffer_alloc_size_msg"); + emitter.instruction(&format!("mov edx, {}", BUFFER_ALLOC_SIZE_MSG.len())); // pass the exact buffer-length diagnostic byte count + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // print the fatal buffer-length message to stderr + emitter.instruction("mov edi, 1"); // exit code 1 for an unrepresentable buffer length + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the buffer-length failure } diff --git a/src/codegen_support/runtime/compare/array_loose_eq.rs b/src/codegen_support/runtime/compare/array_loose_eq.rs new file mode 100644 index 0000000000..dde2025f2f --- /dev/null +++ b/src/codegen_support/runtime/compare/array_loose_eq.rs @@ -0,0 +1,307 @@ +//! Purpose: +//! Emits `__rt_mixed_array_loose_eq`, the runtime implementation of PHP's `==` +//! between two arrays: equal element counts, and for every key of the left array a +//! matching key in the right array whose value is loosely equal. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::compare`. +//! - `__rt_mixed_loose_eq` once both operands unbox to an array-like tag. +//! +//! Key details: +//! - PHP's `==` on arrays is ORDER-INDEPENDENT (unlike `===`): the walk enumerates +//! the left array's keys and looks each one up in the right array, so +//! `["a"=>1,"b"=>2] == ["b"=>2,"a"=>1]` is true while `[1,2] == [2=>1,3=>2]` is +//! false. +//! - Key presence is checked BEFORE the value is read. `__rt_mixed_array_get` +//! answers `null` both for "absent" and for "present but null", so a missing key +//! would otherwise compare equal to a stored `null`. +//! - elephc has two array representations. Tag 4 (indexed) is always the list +//! `0..count-1`, so its keys are enumerated by counting; tag 5 (hash) is walked +//! with the shared `__rt_hash_iter_next` cursor protocol. Both feed the same +//! per-entry comparison block. +//! - Every value read through `__rt_mixed_array_get` is owned by this helper and is +//! released before the next entry. + +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Emits `__rt_mixed_array_loose_eq` for the active target. +/// +/// Input: AArch64 `x0`/`x1` = the two boxed array cells, `x2` = recursion depth; +/// x86_64 `rdi`/`rsi`/`rdx`. Output: `x0` / `rax` = 1 when loosely equal. Both +/// operands stay borrowed. +pub fn emit_mixed_array_loose_eq(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mixed_array_loose_eq_x86_64(emitter); + return; + } + emit_mixed_array_loose_eq_aarch64(emitter); +} + +/// Emits the AArch64 array comparison walker. +/// +/// Frame (128 bytes): `[sp,#0]` left cell, `[sp,#8]` right cell, `[sp,#16]` depth, +/// `[sp,#24]` shared element count, `[sp,#32]`/`[sp,#40]` right tag and payload, +/// `[sp,#48]` index-or-cursor, `[sp,#56]`/`[sp,#64]` left tag and payload, +/// `[sp,#72]`/`[sp,#80]` the current key pair, `[sp,#88]`/`[sp,#96]` the two owned +/// value cells, `[sp,#104]` the comparison result, `[sp,#112]` saved `x29`/`x30`. +fn emit_mixed_array_loose_eq_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_array_loose_eq ---"); + emitter.label_global("__rt_mixed_array_loose_eq"); + + emitter.instruction("sub sp, sp, #128"); // allocate the array comparison frame + emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #112"); // establish the array comparison frame pointer + emitter.instruction("stp x0, x1, [sp, #0]"); // save both boxed array operands + emitter.instruction("str x2, [sp, #16]"); // save the current recursion depth + emitter.instruction("bl __rt_mixed_count"); // count the left array's elements + emitter.instruction("str x0, [sp, #24]"); // save the left element count + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed array operand + emitter.instruction("bl __rt_mixed_count"); // count the right array's elements + emitter.instruction("ldr x9, [sp, #24]"); // reload the left element count + emitter.instruction("cmp x9, x0"); // PHP requires both arrays to hold the same number of entries + emitter.instruction("b.ne __rt_male_false"); // different sizes are never loosely equal + emitter.instruction("cbz x9, __rt_male_true"); // two empty arrays are loosely equal + + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed array operand + emitter.instruction("bl __rt_mixed_unbox"); // left cell -> x0=tag, x1=payload pointer + emitter.instruction("str x0, [sp, #56]"); // save the left container tag + emitter.instruction("str x1, [sp, #64]"); // save the left container payload pointer + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed array operand + emitter.instruction("bl __rt_mixed_unbox"); // right cell -> x0=tag, x1=payload pointer + emitter.instruction("str x0, [sp, #32]"); // save the right container tag + emitter.instruction("str x1, [sp, #40]"); // save the right container payload pointer + emitter.instruction("mov x11, #0"); // the index/cursor starts at the first entry + emitter.instruction("str x11, [sp, #48]"); // save the initial index/cursor + emitter.instruction("ldr x9, [sp, #56]"); // reload the left container tag + emitter.instruction("cmp x9, #4"); // is the left container an indexed array? + emitter.instruction("b.ne __rt_male_hash_loop"); // hashes enumerate through the iterator protocol + + // -- indexed arrays are the list 0..count-1, so keys come from the index -- + emitter.label("__rt_male_indexed_loop"); + emitter.instruction("ldr x11, [sp, #48]"); // reload the current element index + emitter.instruction("ldr x12, [sp, #24]"); // reload the shared element count + emitter.instruction("cmp x11, x12"); // has every element been compared? + emitter.instruction("b.ge __rt_male_true"); // every key matched loosely + emitter.instruction("str x11, [sp, #72]"); // the element index is the PHP key + emitter.instruction("mov x13, #-1"); // key_hi = -1 marks an integer key + emitter.instruction("str x13, [sp, #80]"); // save the integer-key marker + emitter.instruction("b __rt_male_entry"); // compare this key against the right array + + emitter.label("__rt_male_indexed_next"); + emitter.instruction("ldr x11, [sp, #48]"); // reload the current element index + emitter.instruction("add x11, x11, #1"); // advance to the next list slot + emitter.instruction("str x11, [sp, #48]"); // save the advanced element index + emitter.instruction("b __rt_male_indexed_loop"); // keep walking the list + + // -- hashes enumerate in insertion order through the shared cursor protocol -- + emitter.label("__rt_male_hash_loop"); + emitter.instruction("ldr x0, [sp, #64]"); // reload the left hash payload pointer + emitter.instruction("ldr x1, [sp, #48]"); // reload the iteration cursor + emitter.instruction("bl __rt_hash_iter_next"); // x0=next cursor, x1=key pointer, x2=key length + emitter.instruction("cmp x0, #-1"); // has the walk consumed every entry? + emitter.instruction("b.eq __rt_male_true"); // every key matched loosely + emitter.instruction("str x0, [sp, #48]"); // save the next iteration cursor + emitter.instruction("str x1, [sp, #72]"); // save the current key low word + emitter.instruction("str x2, [sp, #80]"); // save the current key high word + emitter.instruction("b __rt_male_entry"); // compare this key against the right array + + emitter.label("__rt_male_hash_next"); + emitter.instruction("b __rt_male_hash_loop"); // keep walking the hash in insertion order + + // -- one key: it must exist on the right and hold a loosely equal value -- + emitter.label("__rt_male_entry"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the right container tag + emitter.instruction("cmp x9, #4"); // is the right container an indexed array? + emitter.instruction("b.ne __rt_male_entry_hash_lookup"); // hashes answer key presence themselves + emitter.instruction("ldr x13, [sp, #80]"); // reload the current key high word + emitter.instruction("cmp x13, #-1"); // is the key an integer key? + emitter.instruction("b.ne __rt_male_false"); // a string key cannot exist in a list + emitter.instruction("ldr x13, [sp, #72]"); // reload the current key low word + emitter.instruction("cmp x13, #0"); // is the integer key non-negative? + emitter.instruction("b.lt __rt_male_false"); // a negative key is outside every list + emitter.instruction("ldr x12, [sp, #24]"); // reload the shared element count + emitter.instruction("cmp x13, x12"); // is the integer key inside the list bounds? + emitter.instruction("b.ge __rt_male_false"); // an out-of-range key is absent + emitter.instruction("b __rt_male_entry_compare"); // the key exists, compare the values + emitter.label("__rt_male_entry_hash_lookup"); + emitter.instruction("ldr x0, [sp, #40]"); // reload the right hash payload pointer + emitter.instruction("ldr x1, [sp, #72]"); // reload the current key low word + emitter.instruction("ldr x2, [sp, #80]"); // reload the current key high word + emitter.instruction("bl __rt_hash_get"); // probe the right hash for this key + emitter.instruction("cbz x0, __rt_male_false"); // a key missing on the right ends the comparison + + emitter.label("__rt_male_entry_compare"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed array operand + emitter.instruction("ldr x1, [sp, #72]"); // reload the current key low word + emitter.instruction("ldr x2, [sp, #80]"); // reload the current key high word + emitter.instruction("mov x3, #0"); // read quietly: a comparison must not warn + emitter.instruction("bl __rt_mixed_array_get"); // read the left value as an owned boxed cell + emitter.instruction("str x0, [sp, #88]"); // save the owned left value cell + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed array operand + emitter.instruction("ldr x1, [sp, #72]"); // reload the current key low word + emitter.instruction("ldr x2, [sp, #80]"); // reload the current key high word + emitter.instruction("mov x3, #0"); // read quietly: a comparison must not warn + emitter.instruction("bl __rt_mixed_array_get"); // read the right value as an owned boxed cell + emitter.instruction("str x0, [sp, #96]"); // save the owned right value cell + emitter.instruction("ldr x0, [sp, #88]"); // reload the left value cell + emitter.instruction("ldr x1, [sp, #96]"); // reload the right value cell + emitter.instruction("ldr x2, [sp, #16]"); // reload the current recursion depth + emitter.instruction("bl __rt_mixed_loose_eq_d"); // compare the two element values loosely + emitter.instruction("str x0, [sp, #104]"); // save the element comparison result + emitter.instruction("ldr x0, [sp, #88]"); // reload the owned left value cell + emitter.instruction("bl __rt_decref_mixed"); // release the left element copy + emitter.instruction("ldr x0, [sp, #96]"); // reload the owned right value cell + emitter.instruction("bl __rt_decref_mixed"); // release the right element copy + emitter.instruction("ldr x0, [sp, #104]"); // reload the element comparison result + emitter.instruction("cbz x0, __rt_male_false"); // one differing element ends the comparison + emitter.instruction("ldr x9, [sp, #56]"); // reload the left container tag + emitter.instruction("cmp x9, #4"); // did this entry come from the list walk? + emitter.instruction("b.eq __rt_male_indexed_next"); // resume the list walk + emitter.instruction("b __rt_male_hash_next"); // resume the hash walk + + emitter.label("__rt_male_true"); + emitter.instruction("mov x0, #1"); // report that the two arrays are loosely equal + emitter.instruction("b __rt_male_done"); // return the true result + + emitter.label("__rt_male_false"); + emitter.instruction("mov x0, #0"); // report that the two arrays differ + + emitter.label("__rt_male_done"); + emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #128"); // release the array comparison frame + emitter.instruction("ret"); // return the array loose-equality boolean +} + +/// Emits the x86_64 array comparison walker. +/// +/// Frame (112 bytes below `rbp`): `[rbp-8]` left cell, `[rbp-16]` right cell, +/// `[rbp-24]` depth, `[rbp-32]` shared element count, `[rbp-40]`/`[rbp-48]` right +/// tag and payload, `[rbp-56]` index-or-cursor, `[rbp-64]`/`[rbp-72]` left tag and +/// payload, `[rbp-80]`/`[rbp-88]` the current key pair, `[rbp-96]`/`[rbp-104]` the +/// two owned value cells, `[rbp-112]` the comparison result. +fn emit_mixed_array_loose_eq_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_array_loose_eq ---"); + emitter.label_global("__rt_mixed_array_loose_eq"); + + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the array comparison frame pointer + emitter.instruction("sub rsp, 112"); // allocate the aligned array comparison frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed array operand + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed array operand + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the current recursion depth + emitter.instruction("mov rax, rdi"); // move the left cell into the count input register + abi::emit_call_label(emitter, "__rt_mixed_count"); // count the left array's elements + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the left element count + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right cell for counting + abi::emit_call_label(emitter, "__rt_mixed_count"); // count the right array's elements + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left element count + emitter.instruction("cmp r10, rax"); // PHP requires both arrays to hold the same number of entries + emitter.instruction("jne __rt_male_false"); // different sizes are never loosely equal + emitter.instruction("test r10, r10"); // are both arrays empty? + emitter.instruction("jz __rt_male_true"); // two empty arrays are loosely equal + + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left cell for unboxing + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // left cell -> rax=tag, rdi=payload pointer + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the left container tag + emitter.instruction("mov QWORD PTR [rbp - 72], rdi"); // save the left container payload pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right cell for unboxing + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // right cell -> rax=tag, rdi=payload pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the right container tag + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the right container payload pointer + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // the index/cursor starts at the first entry + emitter.instruction("cmp QWORD PTR [rbp - 64], 4"); // is the left container an indexed array? + emitter.instruction("jne __rt_male_hash_loop"); // hashes enumerate through the iterator protocol + + // -- indexed arrays are the list 0..count-1, so keys come from the index -- + emitter.label("__rt_male_indexed_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the current element index + emitter.instruction("cmp r10, QWORD PTR [rbp - 32]"); // has every element been compared? + emitter.instruction("jge __rt_male_true"); // every key matched loosely + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // the element index is the PHP key + emitter.instruction("mov QWORD PTR [rbp - 88], -1"); // key_hi = -1 marks an integer key + emitter.instruction("jmp __rt_male_entry"); // compare this key against the right array + + emitter.label("__rt_male_indexed_next"); + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the current element index + emitter.instruction("add r10, 1"); // advance to the next list slot + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the advanced element index + emitter.instruction("jmp __rt_male_indexed_loop"); // keep walking the list + + // -- hashes enumerate in insertion order through the shared cursor protocol -- + emitter.label("__rt_male_hash_loop"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // reload the left hash payload pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the iteration cursor + abi::emit_call_label(emitter, "__rt_hash_iter_next"); // rax=next cursor, rdi=key pointer, rdx=key length + emitter.instruction("cmp rax, -1"); // has the walk consumed every entry? + emitter.instruction("je __rt_male_true"); // every key matched loosely + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the next iteration cursor + emitter.instruction("mov QWORD PTR [rbp - 80], rdi"); // save the current key low word + emitter.instruction("mov QWORD PTR [rbp - 88], rdx"); // save the current key high word + emitter.instruction("jmp __rt_male_entry"); // compare this key against the right array + + emitter.label("__rt_male_hash_next"); + emitter.instruction("jmp __rt_male_hash_loop"); // keep walking the hash in insertion order + + // -- one key: it must exist on the right and hold a loosely equal value -- + emitter.label("__rt_male_entry"); + emitter.instruction("cmp QWORD PTR [rbp - 40], 4"); // is the right container an indexed array? + emitter.instruction("jne __rt_male_entry_hash_lookup"); // hashes answer key presence themselves + emitter.instruction("cmp QWORD PTR [rbp - 88], -1"); // is the key an integer key? + emitter.instruction("jne __rt_male_false"); // a string key cannot exist in a list + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the current key low word + emitter.instruction("cmp r10, 0"); // is the integer key non-negative? + emitter.instruction("jl __rt_male_false"); // a negative key is outside every list + emitter.instruction("cmp r10, QWORD PTR [rbp - 32]"); // is the integer key inside the list bounds? + emitter.instruction("jge __rt_male_false"); // an out-of-range key is absent + emitter.instruction("jmp __rt_male_entry_compare"); // the key exists, compare the values + emitter.label("__rt_male_entry_hash_lookup"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the right hash payload pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // reload the current key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // reload the current key high word + abi::emit_call_label(emitter, "__rt_hash_get"); // probe the right hash for this key + emitter.instruction("test rax, rax"); // did the right hash contain this key? + emitter.instruction("jz __rt_male_false"); // a key missing on the right ends the comparison + + emitter.label("__rt_male_entry_compare"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed array operand + emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // reload the current key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // reload the current key high word + emitter.instruction("xor ecx, ecx"); // read quietly: a comparison must not warn + abi::emit_call_label(emitter, "__rt_mixed_array_get"); // read the left value as an owned boxed cell + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // save the owned left value cell + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed array operand + emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // reload the current key low word + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // reload the current key high word + emitter.instruction("xor ecx, ecx"); // read quietly: a comparison must not warn + abi::emit_call_label(emitter, "__rt_mixed_array_get"); // read the right value as an owned boxed cell + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save the owned right value cell + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // reload the left value cell + emitter.instruction("mov rsi, QWORD PTR [rbp - 104]"); // reload the right value cell + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the current recursion depth + abi::emit_call_label(emitter, "__rt_mixed_loose_eq_d"); // compare the two element values loosely + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the element comparison result + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // reload the owned left value cell + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the left element copy + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload the owned right value cell + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the right element copy + emitter.instruction("cmp QWORD PTR [rbp - 112], 0"); // did the two element values compare equal? + emitter.instruction("je __rt_male_false"); // one differing element ends the comparison + emitter.instruction("cmp QWORD PTR [rbp - 64], 4"); // did this entry come from the list walk? + emitter.instruction("je __rt_male_indexed_next"); // resume the list walk + emitter.instruction("jmp __rt_male_hash_next"); // resume the hash walk + + emitter.label("__rt_male_true"); + emitter.instruction("mov rax, 1"); // report that the two arrays are loosely equal + emitter.instruction("jmp __rt_male_done"); // return the true result + + emitter.label("__rt_male_false"); + emitter.instruction("xor rax, rax"); // report that the two arrays differ + + emitter.label("__rt_male_done"); + emitter.instruction("add rsp, 112"); // release the array comparison frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the array loose-equality boolean +} diff --git a/src/codegen_support/runtime/compare/mixed_loose_eq.rs b/src/codegen_support/runtime/compare/mixed_loose_eq.rs new file mode 100644 index 0000000000..fea30ace66 --- /dev/null +++ b/src/codegen_support/runtime/compare/mixed_loose_eq.rs @@ -0,0 +1,555 @@ +//! Purpose: +//! Emits `__rt_mixed_loose_eq`, the runtime implementation of PHP's `==` for two +//! boxed `Mixed` cells. It encodes PHP 8's comparison table end to end: bool/null +//! coercion, numeric-string parsing, array-vs-array structural comparison and +//! object-vs-object class/property comparison. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::compare`. +//! - Generated code, through the `lower_loose_eq` fallback in +//! `crate::codegen::lower_inst::comparisons`, which boxes both operands first. +//! +//! Key details: +//! - PHP 8 rule order, and why it matters: a `bool` operand wins over everything +//! (`[0] == true`), then `null` (`[] == null` is true, `null == "0"` is false +//! because null converts to `""`, not to `0`), then containers (an array is +//! loosely equal only to another array), then objects, then strings, then a +//! numeric comparison. Reordering any two of those changes observable results. +//! - `__rt_mixed_loose_eq_d` carries the recursion depth; `__rt_mixed_loose_eq` is +//! the depth-0 entry point every caller uses. +//! - Same-tag int/resource/callable payloads compare word-for-word rather than +//! through `double`, so large integers do not lose precision. + +use super::MAX_LOOSE_EQ_DEPTH; +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Emits `__rt_mixed_loose_eq` / `__rt_mixed_loose_eq_d` for the active target. +/// +/// Public entry: AArch64 `x0`/`x1` = the two boxed `Mixed` operands, result in `x0`; +/// x86_64 `rdi`/`rsi`, result in `rax`. The `_d` form takes the recursion depth in +/// `x2` / `rdx`. Both operands stay borrowed: the helper never releases them. +pub fn emit_mixed_loose_eq(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mixed_loose_eq_x86_64(emitter); + return; + } + emit_mixed_loose_eq_aarch64(emitter); +} + +/// Emits the AArch64 loose-equality dispatcher. +/// +/// Frame (112 bytes): `[sp,#0]` left cell, `[sp,#8]` right cell, `[sp,#16]` depth, +/// `[sp,#24..#40]` left tag/lo/hi, `[sp,#48..#64]` right tag/lo/hi, `[sp,#72]` a +/// scalar scratch slot, `[sp,#96]` saved `x29`/`x30`. +fn emit_mixed_loose_eq_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_loose_eq ---"); + emitter.label_global("__rt_mixed_loose_eq"); + emitter.instruction("mov x2, #0"); // every public comparison starts at recursion depth zero + emitter.instruction("b __rt_mixed_loose_eq_d"); // share the depth-carrying comparison body + + emitter.label_global("__rt_mixed_loose_eq_d"); + emitter.instruction("sub sp, sp, #112"); // allocate the loose-equality comparison frame + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #96"); // establish the comparison frame pointer + emitter.instruction("stp x0, x1, [sp, #0]"); // save both boxed operands for the later helper calls + emitter.instruction("str x2, [sp, #16]"); // save the current recursion depth + abi::emit_load_int_immediate(emitter, "x9", MAX_LOOSE_EQ_DEPTH); + emitter.instruction("cmp x2, x9"); // has the walk reached the cyclic-structure cap? + emitter.instruction("b.ge __rt_mle_false"); // stop instead of recursing into a cycle forever + + // -- unbox both operands into concrete tag/payload triples -- + emitter.instruction("bl __rt_mixed_unbox"); // left cell -> x0=tag, x1=lo, x2=hi + emitter.instruction("str x0, [sp, #24]"); // save the left runtime tag + emitter.instruction("stp x1, x2, [sp, #32]"); // save the left payload words + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_unbox"); // right cell -> x0=tag, x1=lo, x2=hi + emitter.instruction("str x0, [sp, #48]"); // save the right runtime tag + emitter.instruction("stp x1, x2, [sp, #56]"); // save the right payload words + + emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime tag for dispatch + emitter.instruction("mov x10, x0"); // keep the right runtime tag in a scratch register + + // -- PHP rule 1: a bool operand converts BOTH sides to bool -- + emitter.instruction("cmp x9, #3"); // is the left operand a bool? + emitter.instruction("b.eq __rt_mle_bool"); // bool comparisons use truthiness on both sides + emitter.instruction("cmp x10, #3"); // is the right operand a bool? + emitter.instruction("b.eq __rt_mle_bool"); // bool comparisons use truthiness on both sides + + // -- PHP rule 2: null converts to "" against a string and to bool otherwise -- + emitter.instruction("cmp x9, #8"); // is the left operand PHP null? + emitter.instruction("b.eq __rt_mle_left_null"); // null has its own conversion rules + emitter.instruction("cmp x10, #8"); // is the right operand PHP null? + emitter.instruction("b.eq __rt_mle_right_null"); // null has its own conversion rules + + // -- PHP rule 3: an array is loosely equal only to another array -- + emitter.instruction("sub x11, x9, #4"); // normalize the left tag against the array range + emitter.instruction("cmp x11, #1"); // tags 4 and 5 are indexed arrays and hashes + emitter.instruction("cset x11, ls"); // record whether the left operand is array-like + emitter.instruction("sub x12, x10, #4"); // normalize the right tag against the array range + emitter.instruction("cmp x12, #1"); // tags 4 and 5 are indexed arrays and hashes + emitter.instruction("cset x12, ls"); // record whether the right operand is array-like + emitter.instruction("and x13, x11, x12"); // are both operands array-like? + emitter.instruction("cbnz x13, __rt_mle_arrays"); // two arrays compare structurally + emitter.instruction("orr x13, x11, x12"); // is exactly one operand array-like? + emitter.instruction("cbnz x13, __rt_mle_false"); // an array never equals a non-array here + + // -- PHP rule 4: objects compare by class then by properties -- + emitter.instruction("cmp x9, #6"); // is the left operand an object? + emitter.instruction("b.ne __rt_mle_left_not_object"); // fall through to the string/number rules + emitter.instruction("cmp x10, #6"); // is the right operand also an object? + emitter.instruction("b.eq __rt_mle_objects"); // two objects compare class-then-property + emitter.instruction("b __rt_mle_object_vs_number_right"); // an object only equals the number 1 + emitter.label("__rt_mle_left_not_object"); + emitter.instruction("cmp x10, #6"); // is the right operand an object? + emitter.instruction("b.eq __rt_mle_object_vs_number_left"); // an object only equals the number 1 + + // -- PHP rule 5: strings compare with numeric-string promotion -- + emitter.instruction("cmp x9, #1"); // is the left operand a string? + emitter.instruction("b.ne __rt_mle_left_not_string"); // only the right operand can still be a string + emitter.instruction("cmp x10, #1"); // is the right operand also a string? + emitter.instruction("b.eq __rt_mle_strings"); // two strings use PHP loose string equality + emitter.instruction("b __rt_mle_left_string_number"); // a string vs a number parses the string + emitter.label("__rt_mle_left_not_string"); + emitter.instruction("cmp x10, #1"); // is the right operand a string? + emitter.instruction("b.eq __rt_mle_right_string_number"); // a number vs a string parses the string + emitter.instruction("b __rt_mle_numeric"); // everything left over compares numerically + + // -- bool coercion of both operands -- + emitter.label("__rt_mle_bool"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand + emitter.instruction("bl __rt_mixed_cast_bool"); // PHP truthiness of the left operand + emitter.instruction("str x0, [sp, #72]"); // save the left truthiness result + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_cast_bool"); // PHP truthiness of the right operand + emitter.instruction("ldr x9, [sp, #72]"); // reload the left truthiness result + emitter.instruction("cmp x9, x0"); // compare the two truthiness values + emitter.instruction("cset x0, eq"); // materialize the bool-coerced equality + emitter.instruction("b __rt_mle_done"); // return the bool-coerced result + + // -- null on the left -- + emitter.label("__rt_mle_left_null"); + emitter.instruction("ldr x10, [sp, #48]"); // reload the right runtime tag + emitter.instruction("cmp x10, #8"); // is the right operand also null? + emitter.instruction("b.eq __rt_mle_true"); // null is loosely equal to null + emitter.instruction("cmp x10, #1"); // is the right operand a string? + emitter.instruction("b.ne __rt_mle_null_vs_right_bool"); // non-string operands coerce to bool + emitter.instruction("ldr x11, [sp, #64]"); // load the right string length + emitter.instruction("cmp x11, #0"); // null converts to "" and equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the null-versus-string result + emitter.instruction("b __rt_mle_done"); // return the null-versus-string result + emitter.label("__rt_mle_null_vs_right_bool"); + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_cast_bool"); // PHP truthiness of the right operand + emitter.instruction("eor x0, x0, #1"); // null equals exactly the falsy values + emitter.instruction("b __rt_mle_done"); // return the null-coerced result + + // -- null on the right -- + emitter.label("__rt_mle_right_null"); + emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime tag + emitter.instruction("cmp x9, #1"); // is the left operand a string? + emitter.instruction("b.ne __rt_mle_null_vs_left_bool"); // non-string operands coerce to bool + emitter.instruction("ldr x11, [sp, #40]"); // load the left string length + emitter.instruction("cmp x11, #0"); // null converts to "" and equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the string-versus-null result + emitter.instruction("b __rt_mle_done"); // return the string-versus-null result + emitter.label("__rt_mle_null_vs_left_bool"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand + emitter.instruction("bl __rt_mixed_cast_bool"); // PHP truthiness of the left operand + emitter.instruction("eor x0, x0, #1"); // null equals exactly the falsy values + emitter.instruction("b __rt_mle_done"); // return the null-coerced result + + // -- container and object recursion -- + emitter.label("__rt_mle_arrays"); + emitter.instruction("ldp x0, x1, [sp, #0]"); // reload both boxed array operands + emitter.instruction("ldr x2, [sp, #16]"); // reload the current recursion depth + emitter.instruction("add x2, x2, #1"); // descend one level for the element walk + emitter.instruction("bl __rt_mixed_array_loose_eq"); // compare the two arrays entry by entry + emitter.instruction("b __rt_mle_done"); // return the structural array result + + emitter.label("__rt_mle_objects"); + emitter.instruction("ldr x0, [sp, #32]"); // reload the left object pointer + emitter.instruction("ldr x1, [sp, #56]"); // reload the right object pointer + emitter.instruction("ldr x2, [sp, #16]"); // reload the current recursion depth + emitter.instruction("add x2, x2, #1"); // descend one level for the property walk + emitter.instruction("bl __rt_obj_loose_eq"); // compare class identity then properties + emitter.instruction("b __rt_mle_done"); // return the object comparison result + + // -- PHP converts an object to the integer 1 when compared with a number -- + emitter.label("__rt_mle_object_vs_number_right"); + emitter.instruction("ldr x10, [sp, #48]"); // reload the right runtime tag + emitter.instruction("cmp x10, #0"); // is the right operand an int? + emitter.instruction("b.eq __rt_mle_object_one_right"); // compare the number against 1 + emitter.instruction("cmp x10, #2"); // is the right operand a float? + emitter.instruction("b.ne __rt_mle_false"); // an object never equals a string or resource + emitter.label("__rt_mle_object_one_right"); + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("fmov d1, #1.0"); // PHP converts the object operand to 1 + emitter.instruction("fcmp d0, d1"); // compare the number against the converted object + emitter.instruction("cset x0, eq"); // materialize the object-versus-number result + emitter.instruction("b __rt_mle_done"); // return the object-versus-number result + + emitter.label("__rt_mle_object_vs_number_left"); + emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime tag + emitter.instruction("cmp x9, #0"); // is the left operand an int? + emitter.instruction("b.eq __rt_mle_object_one_left"); // compare the number against 1 + emitter.instruction("cmp x9, #2"); // is the left operand a float? + emitter.instruction("b.ne __rt_mle_false"); // an object never equals a string or resource + emitter.label("__rt_mle_object_one_left"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the left operand + emitter.instruction("fmov d1, #1.0"); // PHP converts the object operand to 1 + emitter.instruction("fcmp d0, d1"); // compare the number against the converted object + emitter.instruction("cset x0, eq"); // materialize the number-versus-object result + emitter.instruction("b __rt_mle_done"); // return the number-versus-object result + + // -- string comparisons -- + emitter.label("__rt_mle_strings"); + emitter.instruction("ldp x1, x2, [sp, #32]"); // reload the left string pointer and length + emitter.instruction("ldp x3, x4, [sp, #56]"); // reload the right string pointer and length + emitter.instruction("bl __rt_str_loose_eq"); // apply PHP's numeric-string string equality + emitter.instruction("b __rt_mle_done"); // return the string comparison result + + emitter.label("__rt_mle_left_string_number"); + emitter.instruction("ldr x10, [sp, #48]"); // reload the right runtime tag + emitter.instruction("cmp x10, #0"); // is the right operand an int? + emitter.instruction("b.eq __rt_mle_left_string_parse"); // parse the string for a numeric comparison + emitter.instruction("cmp x10, #2"); // is the right operand a float? + emitter.instruction("b.ne __rt_mle_false"); // strings never equal resources or callables + emitter.label("__rt_mle_left_string_parse"); + emitter.instruction("ldp x1, x2, [sp, #32]"); // reload the left string pointer and length + emitter.instruction("bl __rt_str_to_number"); // parse the left string under PHP numeric rules + emitter.instruction("cbz x0, __rt_mle_false"); // a non-numeric string never equals a number + emitter.instruction("str d0, [sp, #72]"); // save the parsed left numeric value + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("ldr d1, [sp, #72]"); // reload the parsed left numeric value + emitter.instruction("fcmp d1, d0"); // compare the parsed string against the number + emitter.instruction("cset x0, eq"); // materialize the string-versus-number result + emitter.instruction("b __rt_mle_done"); // return the string-versus-number result + + emitter.label("__rt_mle_right_string_number"); + emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime tag + emitter.instruction("cmp x9, #0"); // is the left operand an int? + emitter.instruction("b.eq __rt_mle_right_string_parse"); // parse the string for a numeric comparison + emitter.instruction("cmp x9, #2"); // is the left operand a float? + emitter.instruction("b.ne __rt_mle_false"); // strings never equal resources or callables + emitter.label("__rt_mle_right_string_parse"); + emitter.instruction("ldp x1, x2, [sp, #56]"); // reload the right string pointer and length + emitter.instruction("bl __rt_str_to_number"); // parse the right string under PHP numeric rules + emitter.instruction("cbz x0, __rt_mle_false"); // a non-numeric string never equals a number + emitter.instruction("str d0, [sp, #72]"); // save the parsed right numeric value + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the left operand + emitter.instruction("ldr d1, [sp, #72]"); // reload the parsed right numeric value + emitter.instruction("fcmp d0, d1"); // compare the number against the parsed string + emitter.instruction("cset x0, eq"); // materialize the number-versus-string result + emitter.instruction("b __rt_mle_done"); // return the number-versus-string result + + // -- numeric and identity fallbacks -- + emitter.label("__rt_mle_numeric"); + emitter.instruction("ldr x9, [sp, #24]"); // reload the left runtime tag + emitter.instruction("ldr x10, [sp, #48]"); // reload the right runtime tag + emitter.instruction("cmp x9, x10"); // do both operands carry the same runtime tag? + emitter.instruction("b.ne __rt_mle_numeric_float"); // mixed int/float pairs promote to double + emitter.instruction("cmp x9, #2"); // is the shared tag float? + emitter.instruction("b.eq __rt_mle_numeric_float"); // floats need signed-zero and NaN semantics + emitter.instruction("ldr x11, [sp, #32]"); // reload the left payload word + emitter.instruction("ldr x12, [sp, #56]"); // reload the right payload word + emitter.instruction("cmp x11, x12"); // compare int/resource/callable payloads exactly + emitter.instruction("cset x0, eq"); // materialize the same-tag payload equality + emitter.instruction("b __rt_mle_done"); // return the exact payload comparison + + emitter.label("__rt_mle_numeric_float"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the left operand + emitter.instruction("str d0, [sp, #72]"); // save the left numeric operand + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand + emitter.instruction("bl __rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("ldr d1, [sp, #72]"); // reload the left numeric operand + emitter.instruction("fcmp d1, d0"); // compare both operands as doubles + emitter.instruction("cset x0, eq"); // NaN stays unordered and therefore unequal + emitter.instruction("b __rt_mle_done"); // return the numeric comparison result + + emitter.label("__rt_mle_true"); + emitter.instruction("mov x0, #1"); // report loose equality + emitter.instruction("b __rt_mle_done"); // return the true result + + emitter.label("__rt_mle_false"); + emitter.instruction("mov x0, #0"); // report loose inequality + + emitter.label("__rt_mle_done"); + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the comparison frame + emitter.instruction("ret"); // return the loose-equality boolean +} + +/// Emits the x86_64 loose-equality dispatcher. +/// +/// Frame (80 bytes below `rbp`): `[rbp-8]` left cell, `[rbp-16]` right cell, +/// `[rbp-24]` depth, `[rbp-32..-48]` left tag/lo/hi, `[rbp-56..-72]` right +/// tag/lo/hi, `[rbp-80]` a scalar scratch slot. The `push rbp` plus the 80-byte +/// reservation keep `rsp` 16-byte aligned for the nested libc-backed calls. +fn emit_mixed_loose_eq_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_loose_eq ---"); + emitter.label_global("__rt_mixed_loose_eq"); + emitter.instruction("xor edx, edx"); // every public comparison starts at recursion depth zero + emitter.instruction("jmp __rt_mixed_loose_eq_d"); // share the depth-carrying comparison body + + emitter.label_global("__rt_mixed_loose_eq_d"); + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the comparison frame pointer + emitter.instruction("sub rsp, 80"); // allocate the aligned loose-equality comparison frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed operand + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed operand + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the current recursion depth + abi::emit_load_int_immediate(emitter, "r10", MAX_LOOSE_EQ_DEPTH); + emitter.instruction("cmp rdx, r10"); // has the walk reached the cyclic-structure cap? + emitter.instruction("jge __rt_mle_false"); // stop instead of recursing into a cycle forever + + // -- unbox both operands into concrete tag/payload triples -- + emitter.instruction("mov rax, rdi"); // move the left cell into the unbox input register + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // left cell -> rax=tag, rdi=lo, rdx=hi + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the left runtime tag + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the left payload low word + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the left payload high word + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right cell for unboxing + abi::emit_call_label(emitter, "__rt_mixed_unbox"); // right cell -> rax=tag, rdi=lo, rdx=hi + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the right runtime tag + emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // save the right payload low word + emitter.instruction("mov QWORD PTR [rbp - 72], rdx"); // save the right payload high word + + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left runtime tag for dispatch + emitter.instruction("mov r11, rax"); // keep the right runtime tag in a scratch register + + // -- PHP rule 1: a bool operand converts BOTH sides to bool -- + emitter.instruction("cmp r10, 3"); // is the left operand a bool? + emitter.instruction("je __rt_mle_bool"); // bool comparisons use truthiness on both sides + emitter.instruction("cmp r11, 3"); // is the right operand a bool? + emitter.instruction("je __rt_mle_bool"); // bool comparisons use truthiness on both sides + + // -- PHP rule 2: null converts to "" against a string and to bool otherwise -- + emitter.instruction("cmp r10, 8"); // is the left operand PHP null? + emitter.instruction("je __rt_mle_left_null"); // null has its own conversion rules + emitter.instruction("cmp r11, 8"); // is the right operand PHP null? + emitter.instruction("je __rt_mle_right_null"); // null has its own conversion rules + + // -- PHP rule 3: an array is loosely equal only to another array -- + emitter.instruction("lea rax, [r10 - 4]"); // normalize the left tag against the array range + emitter.instruction("cmp rax, 1"); // tags 4 and 5 are indexed arrays and hashes + emitter.instruction("setbe al"); // record whether the left operand is array-like + emitter.instruction("movzx rax, al"); // widen the left array-like predicate + emitter.instruction("lea rcx, [r11 - 4]"); // normalize the right tag against the array range + emitter.instruction("cmp rcx, 1"); // tags 4 and 5 are indexed arrays and hashes + emitter.instruction("setbe cl"); // record whether the right operand is array-like + emitter.instruction("movzx rcx, cl"); // widen the right array-like predicate + emitter.instruction("mov rsi, rax"); // copy the left predicate for the combined tests + emitter.instruction("and rsi, rcx"); // are both operands array-like? + emitter.instruction("jnz __rt_mle_arrays"); // two arrays compare structurally + emitter.instruction("or rax, rcx"); // is exactly one operand array-like? + emitter.instruction("jnz __rt_mle_false"); // an array never equals a non-array here + + // -- PHP rule 4: objects compare by class then by properties -- + emitter.instruction("cmp r10, 6"); // is the left operand an object? + emitter.instruction("jne __rt_mle_left_not_object"); // fall through to the string/number rules + emitter.instruction("cmp r11, 6"); // is the right operand also an object? + emitter.instruction("je __rt_mle_objects"); // two objects compare class-then-property + emitter.instruction("jmp __rt_mle_object_vs_number_right"); // an object only equals the number 1 + emitter.label("__rt_mle_left_not_object"); + emitter.instruction("cmp r11, 6"); // is the right operand an object? + emitter.instruction("je __rt_mle_object_vs_number_left"); // an object only equals the number 1 + + // -- PHP rule 5: strings compare with numeric-string promotion -- + emitter.instruction("cmp r10, 1"); // is the left operand a string? + emitter.instruction("jne __rt_mle_left_not_string"); // only the right operand can still be a string + emitter.instruction("cmp r11, 1"); // is the right operand also a string? + emitter.instruction("je __rt_mle_strings"); // two strings use PHP loose string equality + emitter.instruction("jmp __rt_mle_left_string_number"); // a string vs a number parses the string + emitter.label("__rt_mle_left_not_string"); + emitter.instruction("cmp r11, 1"); // is the right operand a string? + emitter.instruction("je __rt_mle_right_string_number"); // a number vs a string parses the string + emitter.instruction("jmp __rt_mle_numeric"); // everything left over compares numerically + + // -- bool coercion of both operands -- + emitter.label("__rt_mle_bool"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // PHP truthiness of the left operand + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save the left truthiness result + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // PHP truthiness of the right operand + emitter.instruction("cmp QWORD PTR [rbp - 80], rax"); // compare the two truthiness values + emitter.instruction("sete al"); // materialize the bool-coerced equality + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the bool-coerced result + + // -- null on the left -- + emitter.label("__rt_mle_left_null"); + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the right runtime tag + emitter.instruction("cmp r11, 8"); // is the right operand also null? + emitter.instruction("je __rt_mle_true"); // null is loosely equal to null + emitter.instruction("cmp r11, 1"); // is the right operand a string? + emitter.instruction("jne __rt_mle_null_vs_right_bool"); // non-string operands coerce to bool + emitter.instruction("cmp QWORD PTR [rbp - 72], 0"); // null converts to "" and equals only the empty string + emitter.instruction("sete al"); // materialize the null-versus-string result + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the null-versus-string result + emitter.label("__rt_mle_null_vs_right_bool"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // PHP truthiness of the right operand + emitter.instruction("xor rax, 1"); // null equals exactly the falsy values + emitter.instruction("jmp __rt_mle_done"); // return the null-coerced result + + // -- null on the right -- + emitter.label("__rt_mle_right_null"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left runtime tag + emitter.instruction("cmp r10, 1"); // is the left operand a string? + emitter.instruction("jne __rt_mle_null_vs_left_bool"); // non-string operands coerce to bool + emitter.instruction("cmp QWORD PTR [rbp - 48], 0"); // null converts to "" and equals only the empty string + emitter.instruction("sete al"); // materialize the string-versus-null result + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the string-versus-null result + emitter.label("__rt_mle_null_vs_left_bool"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_bool"); // PHP truthiness of the left operand + emitter.instruction("xor rax, 1"); // null equals exactly the falsy values + emitter.instruction("jmp __rt_mle_done"); // return the null-coerced result + + // -- container and object recursion -- + emitter.label("__rt_mle_arrays"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed array operand + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right boxed array operand + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the current recursion depth + emitter.instruction("add rdx, 1"); // descend one level for the element walk + abi::emit_call_label(emitter, "__rt_mixed_array_loose_eq"); // compare the two arrays entry by entry + emitter.instruction("jmp __rt_mle_done"); // return the structural array result + + emitter.label("__rt_mle_objects"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the left object pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // reload the right object pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the current recursion depth + emitter.instruction("add rdx, 1"); // descend one level for the property walk + abi::emit_call_label(emitter, "__rt_obj_loose_eq"); // compare class identity then properties + emitter.instruction("jmp __rt_mle_done"); // return the object comparison result + + // -- PHP converts an object to the integer 1 when compared with a number -- + emitter.label("__rt_mle_object_vs_number_right"); + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the right runtime tag + emitter.instruction("cmp r11, 0"); // is the right operand an int? + emitter.instruction("je __rt_mle_object_one_right"); // compare the number against 1 + emitter.instruction("cmp r11, 2"); // is the right operand a float? + emitter.instruction("jne __rt_mle_false"); // an object never equals a string or resource + emitter.label("__rt_mle_object_one_right"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("jmp __rt_mle_compare_one"); // compare the number against the converted object + + emitter.label("__rt_mle_object_vs_number_left"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left runtime tag + emitter.instruction("cmp r10, 0"); // is the left operand an int? + emitter.instruction("je __rt_mle_object_one_left"); // compare the number against 1 + emitter.instruction("cmp r10, 2"); // is the left operand a float? + emitter.instruction("jne __rt_mle_false"); // an object never equals a string or resource + emitter.label("__rt_mle_object_one_left"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the left operand + + emitter.label("__rt_mle_compare_one"); + emitter.instruction("movabs rax, 0x3ff0000000000000"); // materialize the double bit pattern for 1.0 + emitter.instruction("movq xmm1, rax"); // PHP converts the object operand to 1 + emitter.instruction("ucomisd xmm0, xmm1"); // compare the number against the converted object + emitter.instruction("sete al"); // equality requires matching numeric values + emitter.instruction("setnp cl"); // an unordered NaN comparison is never equal + emitter.instruction("and al, cl"); // combine the ordered and equal predicates + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the object-versus-number result + + // -- string comparisons -- + emitter.label("__rt_mle_strings"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the left string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the left string length + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // reload the right string pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 72]"); // reload the right string length + abi::emit_call_label(emitter, "__rt_str_loose_eq"); // apply PHP's numeric-string string equality + emitter.instruction("jmp __rt_mle_done"); // return the string comparison result + + emitter.label("__rt_mle_left_string_number"); + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the right runtime tag + emitter.instruction("cmp r11, 0"); // is the right operand an int? + emitter.instruction("je __rt_mle_left_string_parse"); // parse the string for a numeric comparison + emitter.instruction("cmp r11, 2"); // is the right operand a float? + emitter.instruction("jne __rt_mle_false"); // strings never equal resources or callables + emitter.label("__rt_mle_left_string_parse"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the left string pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // reload the left string length + abi::emit_call_label(emitter, "__rt_str_to_number"); // parse the left string under PHP numeric rules + emitter.instruction("test rax, rax"); // did the left string parse as fully numeric? + emitter.instruction("jz __rt_mle_false"); // a non-numeric string never equals a number + emitter.instruction("movsd QWORD PTR [rbp - 80], xmm0"); // save the parsed left numeric value + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 80]"); // reload the parsed left numeric value + emitter.instruction("jmp __rt_mle_compare_doubles"); // compare the parsed string against the number + + emitter.label("__rt_mle_right_string_number"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left runtime tag + emitter.instruction("cmp r10, 0"); // is the left operand an int? + emitter.instruction("je __rt_mle_right_string_parse"); // parse the string for a numeric comparison + emitter.instruction("cmp r10, 2"); // is the left operand a float? + emitter.instruction("jne __rt_mle_false"); // strings never equal resources or callables + emitter.label("__rt_mle_right_string_parse"); + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the right string pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the right string length + abi::emit_call_label(emitter, "__rt_str_to_number"); // parse the right string under PHP numeric rules + emitter.instruction("test rax, rax"); // did the right string parse as fully numeric? + emitter.instruction("jz __rt_mle_false"); // a non-numeric string never equals a number + emitter.instruction("movsd QWORD PTR [rbp - 80], xmm0"); // save the parsed right numeric value + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the left operand + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 80]"); // reload the parsed right numeric value + emitter.instruction("jmp __rt_mle_compare_doubles"); // compare the number against the parsed string + + // -- numeric and identity fallbacks -- + emitter.label("__rt_mle_numeric"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the left runtime tag + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the right runtime tag + emitter.instruction("cmp r10, r11"); // do both operands carry the same runtime tag? + emitter.instruction("jne __rt_mle_numeric_float"); // mixed int/float pairs promote to double + emitter.instruction("cmp r10, 2"); // is the shared tag float? + emitter.instruction("je __rt_mle_numeric_float"); // floats need signed-zero and NaN semantics + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the left payload word + emitter.instruction("cmp rax, QWORD PTR [rbp - 64]"); // compare int/resource/callable payloads exactly + emitter.instruction("sete al"); // materialize the same-tag payload equality + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the exact payload comparison + + emitter.label("__rt_mle_numeric_float"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the left boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the left operand + emitter.instruction("movsd QWORD PTR [rbp - 80], xmm0"); // save the left numeric operand + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand + abi::emit_call_label(emitter, "__rt_mixed_cast_float"); // read the numeric value of the right operand + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 80]"); // reload the left numeric operand + + emitter.label("__rt_mle_compare_doubles"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare both operands as doubles + emitter.instruction("sete al"); // equality requires matching numeric values + emitter.instruction("setnp cl"); // an unordered NaN comparison is never equal + emitter.instruction("and al, cl"); // combine the ordered and equal predicates + emitter.instruction("movzx rax, al"); // widen the boolean byte into the result register + emitter.instruction("jmp __rt_mle_done"); // return the numeric comparison result + + emitter.label("__rt_mle_true"); + emitter.instruction("mov rax, 1"); // report loose equality + emitter.instruction("jmp __rt_mle_done"); // return the true result + + emitter.label("__rt_mle_false"); + emitter.instruction("xor rax, rax"); // report loose inequality + + emitter.label("__rt_mle_done"); + emitter.instruction("add rsp, 80"); // release the comparison frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the loose-equality boolean +} diff --git a/src/codegen_support/runtime/compare/mod.rs b/src/codegen_support/runtime/compare/mod.rs new file mode 100644 index 0000000000..3238cfc9ed --- /dev/null +++ b/src/codegen_support/runtime/compare/mod.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Collects the runtime helpers that implement PHP's `==` (loose equality) for +//! values whose shape is only known at run time: boxed `Mixed` cells, arrays and +//! hashes compared pair-wise, and objects compared class-then-property. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()`. +//! +//! Key details: +//! - `__rt_mixed_loose_eq` is the single entry point every backend loose-equality +//! fallback funnels through; the array and object walkers call back into it for +//! element/property comparison, so all three share one PHP comparison table. +//! - `__rt_php_compare` is the *ordering* counterpart (PHP's `<`, `>`, `<=>` table) over +//! unboxed `(tag, lo, hi)` triples; the single-array `min()` / `max()` reductions use it. +//! - Recursion carries an explicit depth argument instead of global state, so the +//! helpers stay reentrant; the cap keeps a cyclic structure from overflowing the +//! stack (see `MAX_LOOSE_EQ_DEPTH`). + +mod array_loose_eq; +mod mixed_loose_eq; +mod obj_loose_eq; +mod php_compare; + +pub use array_loose_eq::emit_mixed_array_loose_eq; +pub use mixed_loose_eq::emit_mixed_loose_eq; +pub use obj_loose_eq::emit_obj_loose_eq; +pub use php_compare::emit_php_compare; + +/// Maximum nesting the loose-equality walkers follow before reporting "not equal". +/// +/// PHP raises `Fatal error: Nesting level too deep - recursive dependency?` when +/// `==` meets a cyclic array/object graph. elephc has no comparable unwind path in +/// a leaf runtime helper, so the walkers stop at this depth and report inequality +/// instead of recursing until the stack dies. Real data nests far below the cap; +/// only a genuine cycle can reach it. +pub(crate) const MAX_LOOSE_EQ_DEPTH: i64 = 256; diff --git a/src/codegen_support/runtime/compare/obj_loose_eq.rs b/src/codegen_support/runtime/compare/obj_loose_eq.rs new file mode 100644 index 0000000000..9d7518b3da --- /dev/null +++ b/src/codegen_support/runtime/compare/obj_loose_eq.rs @@ -0,0 +1,176 @@ +//! Purpose: +//! Emits `__rt_obj_loose_eq`, the runtime implementation of PHP's `==` between two +//! objects: the same instance, or the same class with every declared property +//! loosely equal. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::compare`. +//! - `__rt_mixed_loose_eq` once both operands unbox to the object tag. +//! +//! Key details: +//! - Properties are read through `__rt_obj_prop_count` / `__rt_obj_prop_value`, the +//! same per-class descriptor `var_dump`, `print_r` and `var_export` walk, so the +//! set of compared properties is exactly the set elephc considers to exist. +//! - `__rt_obj_prop_value` hands back an OWNED boxed cell per property, so both +//! sides are released after each comparison; the walker itself only borrows the +//! two receivers. +//! - Enum cases are singletons, so the leading pointer-identity check gives PHP's +//! "enum cases compare by identity" for free before any property walk starts. + +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Emits `__rt_obj_loose_eq` for the active target. +/// +/// Input: AArch64 `x0`/`x1` = the two object pointers, `x2` = recursion depth; +/// x86_64 `rdi`/`rsi`/`rdx`. Output: `x0` / `rax` = 1 when loosely equal. +pub fn emit_obj_loose_eq(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_obj_loose_eq_x86_64(emitter); + return; + } + emit_obj_loose_eq_aarch64(emitter); +} + +/// Emits the AArch64 object comparison walker. +/// +/// Frame (96 bytes): `[sp,#0]` left object, `[sp,#8]` right object, `[sp,#16]` +/// depth, `[sp,#24]` property count, `[sp,#32]` current index, `[sp,#40]` / +/// `[sp,#48]` the two owned property cells, `[sp,#56]` the comparison result, and +/// `[sp,#80]` the saved `x29`/`x30` pair. +fn emit_obj_loose_eq_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_loose_eq ---"); + emitter.label_global("__rt_obj_loose_eq"); + + emitter.instruction("sub sp, sp, #96"); // allocate the object comparison frame + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the object comparison frame pointer + emitter.instruction("cmp x0, x1"); // is this the very same instance? + emitter.instruction("b.eq __rt_ole_true"); // an instance is always loosely equal to itself + emitter.instruction("cbz x0, __rt_ole_false"); // a missing left receiver cannot match + emitter.instruction("cbz x1, __rt_ole_false"); // a missing right receiver cannot match + emitter.instruction("stp x0, x1, [sp, #0]"); // save both receivers for the property walk + emitter.instruction("str x2, [sp, #16]"); // save the current recursion depth + emitter.instruction("ldr x9, [x0]"); // load the left runtime class id + emitter.instruction("ldr x10, [x1]"); // load the right runtime class id + emitter.instruction("cmp x9, x10"); // PHP requires the same class before comparing properties + emitter.instruction("b.ne __rt_ole_false"); // different classes are never loosely equal + emitter.instruction("bl __rt_obj_prop_count"); // count the left receiver's renderable properties + emitter.instruction("str x0, [sp, #24]"); // save the property count + emitter.instruction("mov x11, #0"); // start the property walk at index zero + emitter.instruction("str x11, [sp, #32]"); // save the initial property index + + emitter.label("__rt_ole_loop"); + emitter.instruction("ldr x11, [sp, #32]"); // reload the current property index + emitter.instruction("ldr x12, [sp, #24]"); // reload the property count + emitter.instruction("cmp x11, x12"); // has every property been compared? + emitter.instruction("b.ge __rt_ole_true"); // all properties matched loosely + emitter.instruction("ldr x0, [sp, #0]"); // reload the left receiver + emitter.instruction("mov x1, x11"); // pass the current property index + emitter.instruction("bl __rt_obj_prop_value"); // read the left property as an owned boxed cell + emitter.instruction("str x0, [sp, #40]"); // save the owned left property cell + emitter.instruction("ldr x0, [sp, #8]"); // reload the right receiver + emitter.instruction("ldr x1, [sp, #32]"); // pass the same property index + emitter.instruction("bl __rt_obj_prop_value"); // read the right property as an owned boxed cell + emitter.instruction("str x0, [sp, #48]"); // save the owned right property cell + emitter.instruction("ldr x0, [sp, #40]"); // reload the left property cell + emitter.instruction("ldr x1, [sp, #48]"); // reload the right property cell + emitter.instruction("ldr x2, [sp, #16]"); // reload the current recursion depth + emitter.instruction("bl __rt_mixed_loose_eq_d"); // compare the two property values loosely + emitter.instruction("str x0, [sp, #56]"); // save the property comparison result + emitter.instruction("ldr x0, [sp, #40]"); // reload the owned left property cell + emitter.instruction("bl __rt_decref_mixed"); // release the left property copy + emitter.instruction("ldr x0, [sp, #48]"); // reload the owned right property cell + emitter.instruction("bl __rt_decref_mixed"); // release the right property copy + emitter.instruction("ldr x0, [sp, #56]"); // reload the property comparison result + emitter.instruction("cbz x0, __rt_ole_false"); // one differing property ends the comparison + emitter.instruction("ldr x11, [sp, #32]"); // reload the current property index + emitter.instruction("add x11, x11, #1"); // advance to the next declared property + emitter.instruction("str x11, [sp, #32]"); // save the advanced property index + emitter.instruction("b __rt_ole_loop"); // keep walking the property descriptor + + emitter.label("__rt_ole_true"); + emitter.instruction("mov x0, #1"); // report that the two objects are loosely equal + emitter.instruction("b __rt_ole_done"); // return the true result + + emitter.label("__rt_ole_false"); + emitter.instruction("mov x0, #0"); // report that the two objects differ + + emitter.label("__rt_ole_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the object comparison frame + emitter.instruction("ret"); // return the object loose-equality boolean +} + +/// Emits the x86_64 object comparison walker. +/// +/// Frame (96 bytes below `rbp`): `[rbp-8]` left object, `[rbp-16]` right object, +/// `[rbp-24]` depth, `[rbp-32]` property count, `[rbp-40]` current index, +/// `[rbp-48]` / `[rbp-56]` the two owned property cells, `[rbp-64]` the comparison +/// result. +fn emit_obj_loose_eq_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_loose_eq ---"); + emitter.label_global("__rt_obj_loose_eq"); + + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the object comparison frame pointer + emitter.instruction("sub rsp, 96"); // allocate the aligned object comparison frame + emitter.instruction("cmp rdi, rsi"); // is this the very same instance? + emitter.instruction("je __rt_ole_true"); // an instance is always loosely equal to itself + emitter.instruction("test rdi, rdi"); // is the left receiver missing? + emitter.instruction("jz __rt_ole_false"); // a missing left receiver cannot match + emitter.instruction("test rsi, rsi"); // is the right receiver missing? + emitter.instruction("jz __rt_ole_false"); // a missing right receiver cannot match + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left receiver + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right receiver + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the current recursion depth + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the left runtime class id + emitter.instruction("mov r11, QWORD PTR [rsi]"); // load the right runtime class id + emitter.instruction("cmp r10, r11"); // PHP requires the same class before comparing properties + emitter.instruction("jne __rt_ole_false"); // different classes are never loosely equal + abi::emit_call_label(emitter, "__rt_obj_prop_count"); // count the left receiver's renderable properties + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the property count + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // start the property walk at index zero + + emitter.label("__rt_ole_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current property index + emitter.instruction("cmp r10, QWORD PTR [rbp - 32]"); // has every property been compared? + emitter.instruction("jge __rt_ole_true"); // all properties matched loosely + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left receiver + emitter.instruction("mov rsi, r10"); // pass the current property index + abi::emit_call_label(emitter, "__rt_obj_prop_value"); // read the left property as an owned boxed cell + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the owned left property cell + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right receiver + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the same property index + abi::emit_call_label(emitter, "__rt_obj_prop_value"); // read the right property as an owned boxed cell + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the owned right property cell + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the left property cell + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the right property cell + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the current recursion depth + abi::emit_call_label(emitter, "__rt_mixed_loose_eq_d"); // compare the two property values loosely + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the property comparison result + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the owned left property cell + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the left property copy + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the owned right property cell + abi::emit_call_label(emitter, "__rt_decref_mixed"); // release the right property copy + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // did the two property values compare equal? + emitter.instruction("je __rt_ole_false"); // one differing property ends the comparison + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current property index + emitter.instruction("add r10, 1"); // advance to the next declared property + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the advanced property index + emitter.instruction("jmp __rt_ole_loop"); // keep walking the property descriptor + + emitter.label("__rt_ole_true"); + emitter.instruction("mov rax, 1"); // report that the two objects are loosely equal + emitter.instruction("jmp __rt_ole_done"); // return the true result + + emitter.label("__rt_ole_false"); + emitter.instruction("xor rax, rax"); // report that the two objects differ + + emitter.label("__rt_ole_done"); + emitter.instruction("add rsp, 96"); // release the object comparison frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the object loose-equality boolean +} diff --git a/src/codegen_support/runtime/compare/php_compare.rs b/src/codegen_support/runtime/compare/php_compare.rs new file mode 100644 index 0000000000..8f9b682bd1 --- /dev/null +++ b/src/codegen_support/runtime/compare/php_compare.rs @@ -0,0 +1,645 @@ +//! Purpose: +//! Emits `__rt_php_compare`, the runtime implementation of PHP 8's *ordering* +//! comparison (`zend_compare`, the engine routine behind `<`, `>` and `<=>`) for two +//! unboxed runtime value triples, plus the `__rt_php_truthy` helper it needs for the +//! bool/null coercion rule. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::compare`. +//! - `__rt_min_max_mixed` / `__rt_min_max_str` / `__rt_min_max_hash` +//! (`crate::codegen_support::runtime::arrays::min_max_container`). +//! +//! Key details: +//! - Operands are `(tag, lo, hi)` triples, not boxed cells: tag 0 = int (`lo`), +//! 1 = string (`lo` = bytes, `hi` = length), 2 = float (`lo` = raw `f64` bits), +//! 3 = bool (`lo`), 8 = null. Callers must peel boxed `Mixed` cells with +//! `__rt_mixed_unbox` first. +//! - PHP 8 rule order, which is observable: a `bool` on either side coerces BOTH +//! sides to bool; then `null` (against a string it becomes `""` and a *string* +//! comparison happens, so `null < "0"` but `null == ""`); then two strings use +//! numeric-string promotion; then a number against a string parses the string and, +//! when that fails, compares the number's *string form* byte-wise (`0 < "a"`). +//! - Number-to-string conversion goes through `__rt_itoa` / `__rt_ftoa`, which append +//! to the shared `_concat_buf` scratch. The cursor is saved before and restored +//! after the comparison, so a reduction loop cannot exhaust the buffer. +//! - Known deviations: comparisons that involve a numeric *string* are resolved as +//! `double`s, so two integer strings beyond 2^53 can compare equal where PHP +//! compares them exactly (the same simplification `__rt_mixed_loose_eq` already +//! makes); arrays, objects, resources and callables only rank above the scalar +//! tags and compare equal to each other. + +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Emits `__rt_php_compare` and `__rt_php_truthy` for the active target. +/// +/// `__rt_php_compare` inputs are two runtime value triples — AArch64 +/// `x0`/`x1`/`x2` and `x3`/`x4`/`x5`, x86_64 `rdi`/`rsi`/`rdx` and +/// `rcx`/`r8`/`r9` — and the result is `-1`, `0` or `1` in `x0`/`rax`. +/// String payloads stay borrowed: the helper never releases or persists them. +pub fn emit_php_compare(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_php_truthy_x86_64(emitter); + emit_php_compare_x86_64(emitter); + return; + } + emit_php_truthy_aarch64(emitter); + emit_php_compare_aarch64(emitter); +} + +/// Emits the AArch64 PHP truthiness helper over one runtime value triple. +/// +/// Input `x0` = tag, `x1` = low payload word, `x2` = high payload word; output +/// `x0` = 0 or 1. Leaf routine: it never calls out, so callers only need `x30` +/// saved once. Container, object, resource and callable tags report `true`. +fn emit_php_truthy_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_truthy ---"); + emitter.label_global("__rt_php_truthy"); + + emitter.instruction("cmp x0, #8"); // is the operand PHP null? + emitter.instruction("b.eq __rt_pt_false"); // null is the only always-falsy tag + emitter.instruction("cmp x0, #2"); // is the operand a float? + emitter.instruction("b.eq __rt_pt_float"); // floats need a numeric zero test + emitter.instruction("cmp x0, #1"); // is the operand a string? + emitter.instruction("b.eq __rt_pt_string"); // strings use PHP's "" / "0" rule + emitter.instruction("cmp x0, #0"); // is the operand an int? + emitter.instruction("b.eq __rt_pt_word"); // ints are falsy only at zero + emitter.instruction("cmp x0, #3"); // is the operand a bool? + emitter.instruction("b.eq __rt_pt_word"); // bools carry their truth value in the low word + emitter.instruction("b __rt_pt_true"); // arrays, objects, resources and callables report true here + + emitter.label("__rt_pt_word"); + emitter.instruction("cmp x1, #0"); // compare the integer-like payload against zero + emitter.instruction("cset x0, ne"); // any non-zero integer-like payload is truthy + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_float"); + emitter.instruction("fmov d0, x1"); // reinterpret the payload word as the double it encodes + emitter.instruction("fcmp d0, #0.0"); // compare the double against positive zero + emitter.instruction("cset x0, ne"); // NaN stays unordered and therefore truthy, like PHP + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_string"); + emitter.instruction("cbz x2, __rt_pt_false"); // the empty string is falsy + emitter.instruction("cmp x2, #1"); // only a one-byte string can be the falsy "0" + emitter.instruction("b.ne __rt_pt_true"); // every other non-empty string is truthy + emitter.instruction("ldrb w9, [x1]"); // load the single byte of a one-character string + emitter.instruction("cmp w9, #48"); // is that byte the ASCII digit zero? + emitter.instruction("b.eq __rt_pt_false"); // "0" is PHP's other falsy string + + emitter.label("__rt_pt_true"); + emitter.instruction("mov x0, #1"); // report a truthy operand + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_false"); + emitter.instruction("mov x0, #0"); // report a falsy operand + emitter.instruction("ret"); // return the truthiness flag +} + +/// Emits the AArch64 PHP ordering comparison over two runtime value triples. +/// +/// Frame (128 bytes): `[sp,#0..#16]` left tag/lo/hi, `[sp,#24..#40]` right +/// tag/lo/hi, `[sp,#48]` a parsed-double scratch slot, `[sp,#56]` the +/// "operands were swapped" flag used by the number-versus-string leg, +/// `[sp,#64..#88]` the normalized number/string operands of that leg, +/// `[sp,#96]` the saved `_concat_off` cursor, `[sp,#112]` saved `x29`/`x30`. +fn emit_php_compare_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_compare ---"); + emitter.label_global("__rt_php_compare"); + + emitter.instruction("sub sp, sp, #128"); // allocate the ordering-comparison frame + emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #112"); // establish the comparison frame pointer + emitter.instruction("stp x0, x1, [sp, #0]"); // save the left runtime tag and low payload word + emitter.instruction("str x2, [sp, #16]"); // save the left high payload word + emitter.instruction("stp x3, x4, [sp, #24]"); // save the right runtime tag and low payload word + emitter.instruction("str x5, [sp, #40]"); // save the right high payload word + + // -- PHP rule 1: a bool operand converts BOTH sides to bool -- + emitter.instruction("cmp x0, #3"); // is the left operand a bool? + emitter.instruction("b.eq __rt_pcmp_bools"); // bool comparisons use truthiness on both sides + emitter.instruction("cmp x3, #3"); // is the right operand a bool? + emitter.instruction("b.eq __rt_pcmp_bools"); // bool comparisons use truthiness on both sides + + // -- PHP rule 2: null becomes "" against a string and bool against everything else -- + emitter.instruction("cmp x0, #8"); // is the left operand PHP null? + emitter.instruction("b.eq __rt_pcmp_left_null"); // null has its own conversion rules + emitter.instruction("cmp x3, #8"); // is the right operand PHP null? + emitter.instruction("b.eq __rt_pcmp_right_null"); // null has its own conversion rules + emitter.instruction("b __rt_pcmp_no_null"); // neither operand needs the bool/null coercions + + // -- bool coercion of both operands -- + emitter.label("__rt_pcmp_bools"); + emitter.instruction("bl __rt_php_truthy"); // PHP truthiness of the left operand + emitter.instruction("str x0, [sp, #48]"); // save the left truthiness while the right one is computed + emitter.instruction("ldr x0, [sp, #24]"); // reload the right runtime tag + emitter.instruction("ldr x1, [sp, #32]"); // reload the right low payload word + emitter.instruction("ldr x2, [sp, #40]"); // reload the right high payload word + emitter.instruction("bl __rt_php_truthy"); // PHP truthiness of the right operand + emitter.instruction("ldr x9, [sp, #48]"); // reload the left truthiness value + emitter.instruction("cmp x9, x0"); // false sorts below true in PHP's bool comparison + emitter.instruction("b.lt __rt_pcmp_neg"); // false versus true is "less than" + emitter.instruction("b.gt __rt_pcmp_pos"); // true versus false is "greater than" + emitter.instruction("b __rt_pcmp_zero"); // equal truthiness compares equal + + // -- null on the left -- + emitter.label("__rt_pcmp_left_null"); + emitter.instruction("cmp x3, #8"); // is the right operand also null? + emitter.instruction("b.eq __rt_pcmp_zero"); // null compares equal to null + emitter.instruction("cmp x3, #1"); // is the right operand a string? + emitter.instruction("b.ne __rt_pcmp_null_vs_right"); // non-string operands coerce to bool + emitter.instruction("cbz x5, __rt_pcmp_zero"); // null converts to "" and equals the empty string + emitter.instruction("b __rt_pcmp_neg"); // "" sorts below every non-empty string + emitter.label("__rt_pcmp_null_vs_right"); + emitter.instruction("mov x0, x3"); // pass the right runtime tag to the truthiness helper + emitter.instruction("mov x1, x4"); // pass the right low payload word + emitter.instruction("mov x2, x5"); // pass the right high payload word + emitter.instruction("bl __rt_php_truthy"); // PHP truthiness of the right operand + emitter.instruction("cbz x0, __rt_pcmp_zero"); // null equals every falsy operand + emitter.instruction("b __rt_pcmp_neg"); // null sorts below every truthy operand + + // -- null on the right -- + emitter.label("__rt_pcmp_right_null"); + emitter.instruction("cmp x0, #1"); // is the left operand a string? + emitter.instruction("b.ne __rt_pcmp_left_vs_null"); // non-string operands coerce to bool + emitter.instruction("cbz x2, __rt_pcmp_zero"); // the empty string equals null + emitter.instruction("b __rt_pcmp_pos"); // every non-empty string sorts above "" + emitter.label("__rt_pcmp_left_vs_null"); + emitter.instruction("bl __rt_php_truthy"); // PHP truthiness of the left operand + emitter.instruction("cbz x0, __rt_pcmp_zero"); // every falsy operand equals null + emitter.instruction("b __rt_pcmp_pos"); // every truthy operand sorts above null + + // -- neither operand is bool or null -- + emitter.label("__rt_pcmp_no_null"); + emitter.instruction("cmp x0, #2"); // tags 0, 1 and 2 are the comparable scalar payloads + emitter.instruction("cset x9, ls"); // record whether the left operand is a scalar + emitter.instruction("cmp x3, #2"); // tags 0, 1 and 2 are the comparable scalar payloads + emitter.instruction("cset x10, ls"); // record whether the right operand is a scalar + emitter.instruction("and x11, x9, x10"); // are both operands comparable scalars? + emitter.instruction("cbz x11, __rt_pcmp_nonscalar"); // containers and objects use the coarse ranking below + emitter.instruction("cmp x0, #1"); // is the left operand a string? + emitter.instruction("b.ne __rt_pcmp_left_number"); // only the right operand can still be a string + emitter.instruction("cmp x3, #1"); // is the right operand also a string? + emitter.instruction("b.eq __rt_pcmp_strings"); // two strings use PHP's numeric-string promotion + emitter.instruction("b __rt_pcmp_str_vs_num"); // a string against a number parses the string + emitter.label("__rt_pcmp_left_number"); + emitter.instruction("cmp x3, #1"); // is the right operand a string? + emitter.instruction("b.eq __rt_pcmp_num_vs_str"); // a number against a string parses the string + emitter.instruction("cmp x0, #0"); // is the left operand an int? + emitter.instruction("b.ne __rt_pcmp_numeric"); // any float operand promotes both sides to double + emitter.instruction("cmp x3, #0"); // is the right operand an int? + emitter.instruction("b.ne __rt_pcmp_numeric"); // any float operand promotes both sides to double + emitter.instruction("cmp x1, x4"); // compare two ints exactly, without losing precision + emitter.instruction("b.lt __rt_pcmp_neg"); // the left int is smaller + emitter.instruction("b.gt __rt_pcmp_pos"); // the left int is larger + emitter.instruction("b __rt_pcmp_zero"); // both ints are equal + + // -- containers, objects, resources and callables -- + emitter.label("__rt_pcmp_nonscalar"); + emitter.instruction("orr x11, x9, x10"); // is exactly one operand a comparable scalar? + emitter.instruction("cbz x11, __rt_pcmp_zero"); // two non-scalars are reported equal (documented limitation) + emitter.instruction("cbz x9, __rt_pcmp_pos"); // a non-scalar left operand ranks above a scalar + emitter.instruction("b __rt_pcmp_neg"); // a non-scalar right operand ranks above a scalar + + // -- both operands are numbers: promote to double -- + emitter.label("__rt_pcmp_numeric"); + emitter.instruction("cmp x0, #2"); // is the left operand already a double? + emitter.instruction("b.eq __rt_pcmp_numeric_left_double"); // reinterpret its payload word + emitter.instruction("scvtf d0, x1"); // widen the left int payload into a double + emitter.instruction("b __rt_pcmp_numeric_right"); // continue with the right operand + emitter.label("__rt_pcmp_numeric_left_double"); + emitter.instruction("fmov d0, x1"); // reinterpret the left payload word as a double + emitter.label("__rt_pcmp_numeric_right"); + emitter.instruction("cmp x3, #2"); // is the right operand already a double? + emitter.instruction("b.eq __rt_pcmp_numeric_right_double"); // reinterpret its payload word + emitter.instruction("scvtf d1, x4"); // widen the right int payload into a double + emitter.instruction("b __rt_pcmp_fcmp"); // compare both operands as doubles + emitter.label("__rt_pcmp_numeric_right_double"); + emitter.instruction("fmov d1, x4"); // reinterpret the right payload word as a double + + emitter.label("__rt_pcmp_fcmp"); + emitter.instruction("fcmp d0, d1"); // compare both numeric operands as doubles + emitter.instruction("b.mi __rt_pcmp_neg"); // an ordered less-than result + emitter.instruction("b.eq __rt_pcmp_zero"); // an ordered equal result + emitter.instruction("b __rt_pcmp_pos"); // greater-than, and unordered NaN like PHP's three-way compare + + // -- string versus string -- + emitter.label("__rt_pcmp_strings"); + emitter.instruction("bl __rt_str_to_number"); // parse the left string under PHP's numeric-string grammar + emitter.instruction("cbz x0, __rt_pcmp_str_bytes"); // a non-numeric operand forces the byte comparison + emitter.instruction("str d0, [sp, #48]"); // save the parsed left value across the second parse + emitter.instruction("ldr x1, [sp, #32]"); // reload the right string pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the right string length + emitter.instruction("bl __rt_str_to_number"); // parse the right string under PHP's numeric-string grammar + emitter.instruction("cbz x0, __rt_pcmp_str_bytes"); // a non-numeric operand forces the byte comparison + emitter.instruction("fmov d1, d0"); // move the parsed right value into the comparison register + emitter.instruction("ldr d0, [sp, #48]"); // reload the parsed left value + emitter.instruction("b __rt_pcmp_fcmp"); // two numeric strings compare numerically + emitter.label("__rt_pcmp_str_bytes"); + emitter.instruction("ldr x1, [sp, #8]"); // reload the left string pointer + emitter.instruction("ldr x2, [sp, #16]"); // reload the left string length + emitter.instruction("ldr x3, [sp, #32]"); // reload the right string pointer + emitter.instruction("ldr x4, [sp, #40]"); // reload the right string length + emitter.instruction("bl __rt_strcmp"); // compare both strings byte-wise, then by length + emitter.instruction("cmp x0, #0"); // normalize the byte difference into a three-way result + emitter.instruction("b.lt __rt_pcmp_neg"); // the left string sorts first + emitter.instruction("b.gt __rt_pcmp_pos"); // the right string sorts first + emitter.instruction("b __rt_pcmp_zero"); // both strings are byte-identical + + // -- number versus string, normalized so the number is always the left operand -- + emitter.label("__rt_pcmp_num_vs_str"); + emitter.instruction("str xzr, [sp, #56]"); // the operands are already in number/string order + emitter.instruction("stp x0, x1, [sp, #64]"); // stage the number's tag and payload word + emitter.instruction("stp x4, x5, [sp, #80]"); // stage the string's pointer and length + emitter.instruction("b __rt_pcmp_num_str_body"); // run the shared number-versus-string comparison + emitter.label("__rt_pcmp_str_vs_num"); + emitter.instruction("mov x9, #1"); // the string is the left operand, so the result is negated + emitter.instruction("str x9, [sp, #56]"); // record that the normalized result must be negated + emitter.instruction("stp x3, x4, [sp, #64]"); // stage the number's tag and payload word + emitter.instruction("stp x1, x2, [sp, #80]"); // stage the string's pointer and length + + emitter.label("__rt_pcmp_num_str_body"); + emitter.instruction("ldr x1, [sp, #80]"); // pass the string pointer to the numeric parser + emitter.instruction("ldr x2, [sp, #88]"); // pass the string length to the numeric parser + emitter.instruction("bl __rt_str_to_number"); // parse the string under PHP's numeric-string grammar + emitter.instruction("cbz x0, __rt_pcmp_num_str_bytes"); // PHP 8 compares a number with a non-numeric string as strings + emitter.instruction("str d0, [sp, #48]"); // save the parsed string value + emitter.instruction("ldr x9, [sp, #64]"); // reload the number's runtime tag + emitter.instruction("ldr x10, [sp, #72]"); // reload the number's payload word + emitter.instruction("cmp x9, #2"); // is the number already a double? + emitter.instruction("b.eq __rt_pcmp_num_str_double"); // reinterpret its payload word + emitter.instruction("scvtf d0, x10"); // widen the int payload into a double + emitter.instruction("b __rt_pcmp_num_str_cmp"); // compare the number against the parsed string + emitter.label("__rt_pcmp_num_str_double"); + emitter.instruction("fmov d0, x10"); // reinterpret the payload word as a double + emitter.label("__rt_pcmp_num_str_cmp"); + emitter.instruction("ldr d1, [sp, #48]"); // reload the parsed string value + emitter.instruction("fcmp d0, d1"); // compare the number against the numeric string + emitter.instruction("b.mi __rt_pcmp_maybe_neg"); // the number is smaller, before any swap correction + emitter.instruction("b.eq __rt_pcmp_zero"); // both values are numerically equal + emitter.instruction("b __rt_pcmp_maybe_pos"); // the number is larger, before any swap correction + + emitter.label("__rt_pcmp_num_str_bytes"); + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // read the shared concat scratch cursor + emitter.instruction("str x10, [sp, #96]"); // save it so the rendered number cannot leak scratch space + emitter.instruction("ldr x9, [sp, #64]"); // reload the number's runtime tag + emitter.instruction("ldr x10, [sp, #72]"); // reload the number's payload word + emitter.instruction("cmp x9, #2"); // is the number a double? + emitter.instruction("b.eq __rt_pcmp_num_str_ftoa"); // doubles render through PHP's precision-14 formatter + emitter.instruction("mov x0, x10"); // pass the int payload to the decimal formatter + emitter.instruction("bl __rt_itoa"); // render the int exactly the way PHP casts it to string + emitter.instruction("b __rt_pcmp_num_str_strcmp"); // compare the rendered number with the string + emitter.label("__rt_pcmp_num_str_ftoa"); + emitter.instruction("fmov d0, x10"); // reinterpret the payload word as the double to render + emitter.instruction("bl __rt_ftoa"); // render the double at PHP's default precision of 14 + emitter.label("__rt_pcmp_num_str_strcmp"); + emitter.instruction("ldr x3, [sp, #80]"); // reload the string pointer for the byte comparison + emitter.instruction("ldr x4, [sp, #88]"); // reload the string length for the byte comparison + emitter.instruction("bl __rt_strcmp"); // compare the rendered number with the string byte-wise + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [sp, #96]"); // reload the saved concat scratch cursor + emitter.instruction("str x10, [x9]"); // release the scratch the rendered number occupied + emitter.instruction("cmp x0, #0"); // normalize the byte difference into a three-way result + emitter.instruction("b.lt __rt_pcmp_maybe_neg"); // the rendered number sorts first, before any swap correction + emitter.instruction("b.gt __rt_pcmp_maybe_pos"); // the string sorts first, before any swap correction + emitter.instruction("b __rt_pcmp_zero"); // both byte sequences are identical + + emitter.label("__rt_pcmp_maybe_neg"); + emitter.instruction("ldr x9, [sp, #56]"); // was the string the original left operand? + emitter.instruction("cbz x9, __rt_pcmp_neg"); // no swap: keep the normalized result + emitter.instruction("b __rt_pcmp_pos"); // swapped operands invert the normalized result + emitter.label("__rt_pcmp_maybe_pos"); + emitter.instruction("ldr x9, [sp, #56]"); // was the string the original left operand? + emitter.instruction("cbz x9, __rt_pcmp_pos"); // no swap: keep the normalized result + emitter.instruction("b __rt_pcmp_neg"); // swapped operands invert the normalized result + + emitter.label("__rt_pcmp_neg"); + emitter.instruction("mov x0, #-1"); // the left operand sorts before the right one + emitter.instruction("b __rt_pcmp_done"); // return the three-way result + emitter.label("__rt_pcmp_pos"); + emitter.instruction("mov x0, #1"); // the left operand sorts after the right one + emitter.instruction("b __rt_pcmp_done"); // return the three-way result + emitter.label("__rt_pcmp_zero"); + emitter.instruction("mov x0, #0"); // both operands compare equal + + emitter.label("__rt_pcmp_done"); + emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #128"); // release the ordering-comparison frame + emitter.instruction("ret"); // return the three-way comparison result +} + +/// Emits the x86_64 PHP truthiness helper over one runtime value triple. +/// +/// Input `rdi` = tag, `rsi` = low payload word, `rdx` = high payload word; +/// output `rax` = 0 or 1. Leaf routine, so it needs no frame of its own. +fn emit_php_truthy_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_truthy ---"); + emitter.label_global("__rt_php_truthy"); + + emitter.instruction("cmp rdi, 8"); // is the operand PHP null? + emitter.instruction("je __rt_pt_false"); // null is the only always-falsy tag + emitter.instruction("cmp rdi, 2"); // is the operand a float? + emitter.instruction("je __rt_pt_float"); // floats need a numeric zero test + emitter.instruction("cmp rdi, 1"); // is the operand a string? + emitter.instruction("je __rt_pt_string"); // strings use PHP's "" / "0" rule + emitter.instruction("cmp rdi, 0"); // is the operand an int? + emitter.instruction("je __rt_pt_word"); // ints are falsy only at zero + emitter.instruction("cmp rdi, 3"); // is the operand a bool? + emitter.instruction("je __rt_pt_word"); // bools carry their truth value in the low word + emitter.instruction("jmp __rt_pt_true"); // arrays, objects, resources and callables report true here + + emitter.label("__rt_pt_word"); + emitter.instruction("test rsi, rsi"); // compare the integer-like payload against zero + emitter.instruction("setne al"); // any non-zero integer-like payload is truthy + emitter.instruction("movzx rax, al"); // widen the predicate byte into the result register + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_float"); + emitter.instruction("movq xmm0, rsi"); // reinterpret the payload word as the double it encodes + emitter.instruction("xorpd xmm1, xmm1"); // materialize positive zero for the comparison + emitter.instruction("ucomisd xmm0, xmm1"); // compare the double against positive zero + emitter.instruction("setne al"); // a non-zero double is truthy + emitter.instruction("setp cl"); // an unordered NaN comparison is truthy in PHP too + emitter.instruction("or al, cl"); // combine the non-zero and unordered predicates + emitter.instruction("movzx rax, al"); // widen the predicate byte into the result register + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_string"); + emitter.instruction("test rdx, rdx"); // does the string have any bytes at all? + emitter.instruction("jz __rt_pt_false"); // the empty string is falsy + emitter.instruction("cmp rdx, 1"); // only a one-byte string can be the falsy "0" + emitter.instruction("jne __rt_pt_true"); // every other non-empty string is truthy + emitter.instruction("movzx rax, BYTE PTR [rsi]"); // load the single byte of a one-character string + emitter.instruction("cmp rax, 48"); // is that byte the ASCII digit zero? + emitter.instruction("je __rt_pt_false"); // "0" is PHP's other falsy string + + emitter.label("__rt_pt_true"); + emitter.instruction("mov rax, 1"); // report a truthy operand + emitter.instruction("ret"); // return the truthiness flag + + emitter.label("__rt_pt_false"); + emitter.instruction("xor eax, eax"); // report a falsy operand + emitter.instruction("ret"); // return the truthiness flag +} + +/// Emits the x86_64 PHP ordering comparison over two runtime value triples. +/// +/// Frame (112 bytes below `rbp`): `[rbp-8..-24]` left tag/lo/hi, +/// `[rbp-32..-48]` right tag/lo/hi, `[rbp-56]` a parsed-double scratch slot, +/// `[rbp-64]` the "operands were swapped" flag, `[rbp-72..-96]` the normalized +/// number/string operands, `[rbp-104]` the saved `_concat_off` cursor. The +/// `push rbp` plus the 112-byte reservation keep `rsp` 16-byte aligned for the +/// nested libc-backed calls. +fn emit_php_compare_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_compare ---"); + emitter.label_global("__rt_php_compare"); + + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the comparison frame pointer + emitter.instruction("sub rsp, 112"); // allocate the aligned ordering-comparison frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left runtime tag + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the left low payload word + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the left high payload word + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the right runtime tag + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the right low payload word + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the right high payload word + + // -- PHP rule 1: a bool operand converts BOTH sides to bool -- + emitter.instruction("cmp rdi, 3"); // is the left operand a bool? + emitter.instruction("je __rt_pcmp_bools"); // bool comparisons use truthiness on both sides + emitter.instruction("cmp rcx, 3"); // is the right operand a bool? + emitter.instruction("je __rt_pcmp_bools"); // bool comparisons use truthiness on both sides + + // -- PHP rule 2: null becomes "" against a string and bool against everything else -- + emitter.instruction("cmp rdi, 8"); // is the left operand PHP null? + emitter.instruction("je __rt_pcmp_left_null"); // null has its own conversion rules + emitter.instruction("cmp rcx, 8"); // is the right operand PHP null? + emitter.instruction("je __rt_pcmp_right_null"); // null has its own conversion rules + emitter.instruction("jmp __rt_pcmp_no_null"); // neither operand needs the bool/null coercions + + // -- bool coercion of both operands -- + emitter.label("__rt_pcmp_bools"); + emitter.instruction("call __rt_php_truthy"); // PHP truthiness of the left operand + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the left truthiness while the right one is computed + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the right runtime tag + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reload the right low payload word + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // reload the right high payload word + emitter.instruction("call __rt_php_truthy"); // PHP truthiness of the right operand + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the left truthiness value + emitter.instruction("cmp r10, rax"); // false sorts below true in PHP's bool comparison + emitter.instruction("jl __rt_pcmp_neg"); // false versus true is "less than" + emitter.instruction("jg __rt_pcmp_pos"); // true versus false is "greater than" + emitter.instruction("jmp __rt_pcmp_zero"); // equal truthiness compares equal + + // -- null on the left -- + emitter.label("__rt_pcmp_left_null"); + emitter.instruction("cmp rcx, 8"); // is the right operand also null? + emitter.instruction("je __rt_pcmp_zero"); // null compares equal to null + emitter.instruction("cmp rcx, 1"); // is the right operand a string? + emitter.instruction("jne __rt_pcmp_null_vs_right"); // non-string operands coerce to bool + emitter.instruction("test r9, r9"); // does the right string hold any byte? + emitter.instruction("jz __rt_pcmp_zero"); // null converts to "" and equals the empty string + emitter.instruction("jmp __rt_pcmp_neg"); // "" sorts below every non-empty string + emitter.label("__rt_pcmp_null_vs_right"); + emitter.instruction("mov rdi, rcx"); // pass the right runtime tag to the truthiness helper + emitter.instruction("mov rsi, r8"); // pass the right low payload word + emitter.instruction("mov rdx, r9"); // pass the right high payload word + emitter.instruction("call __rt_php_truthy"); // PHP truthiness of the right operand + emitter.instruction("test rax, rax"); // is the right operand falsy? + emitter.instruction("jz __rt_pcmp_zero"); // null equals every falsy operand + emitter.instruction("jmp __rt_pcmp_neg"); // null sorts below every truthy operand + + // -- null on the right -- + emitter.label("__rt_pcmp_right_null"); + emitter.instruction("cmp rdi, 1"); // is the left operand a string? + emitter.instruction("jne __rt_pcmp_left_vs_null"); // non-string operands coerce to bool + emitter.instruction("test rdx, rdx"); // does the left string hold any byte? + emitter.instruction("jz __rt_pcmp_zero"); // the empty string equals null + emitter.instruction("jmp __rt_pcmp_pos"); // every non-empty string sorts above "" + emitter.label("__rt_pcmp_left_vs_null"); + emitter.instruction("call __rt_php_truthy"); // PHP truthiness of the left operand + emitter.instruction("test rax, rax"); // is the left operand falsy? + emitter.instruction("jz __rt_pcmp_zero"); // every falsy operand equals null + emitter.instruction("jmp __rt_pcmp_pos"); // every truthy operand sorts above null + + // -- neither operand is bool or null -- + emitter.label("__rt_pcmp_no_null"); + emitter.instruction("cmp rdi, 2"); // tags 0, 1 and 2 are the comparable scalar payloads + emitter.instruction("setbe al"); // record whether the left operand is a scalar + emitter.instruction("movzx r10, al"); // widen the left scalar predicate + emitter.instruction("cmp rcx, 2"); // tags 0, 1 and 2 are the comparable scalar payloads + emitter.instruction("setbe al"); // record whether the right operand is a scalar + emitter.instruction("movzx r11, al"); // widen the right scalar predicate + emitter.instruction("mov rax, r10"); // copy the left predicate for the combined tests + emitter.instruction("and rax, r11"); // are both operands comparable scalars? + emitter.instruction("jz __rt_pcmp_nonscalar"); // containers and objects use the coarse ranking below + emitter.instruction("cmp rdi, 1"); // is the left operand a string? + emitter.instruction("jne __rt_pcmp_left_number"); // only the right operand can still be a string + emitter.instruction("cmp rcx, 1"); // is the right operand also a string? + emitter.instruction("je __rt_pcmp_strings"); // two strings use PHP's numeric-string promotion + emitter.instruction("jmp __rt_pcmp_str_vs_num"); // a string against a number parses the string + emitter.label("__rt_pcmp_left_number"); + emitter.instruction("cmp rcx, 1"); // is the right operand a string? + emitter.instruction("je __rt_pcmp_num_vs_str"); // a number against a string parses the string + emitter.instruction("cmp rdi, 0"); // is the left operand an int? + emitter.instruction("jne __rt_pcmp_numeric"); // any float operand promotes both sides to double + emitter.instruction("cmp rcx, 0"); // is the right operand an int? + emitter.instruction("jne __rt_pcmp_numeric"); // any float operand promotes both sides to double + emitter.instruction("cmp rsi, r8"); // compare two ints exactly, without losing precision + emitter.instruction("jl __rt_pcmp_neg"); // the left int is smaller + emitter.instruction("jg __rt_pcmp_pos"); // the left int is larger + emitter.instruction("jmp __rt_pcmp_zero"); // both ints are equal + + // -- containers, objects, resources and callables -- + emitter.label("__rt_pcmp_nonscalar"); + emitter.instruction("mov rax, r10"); // copy the left predicate for the combined tests + emitter.instruction("or rax, r11"); // is exactly one operand a comparable scalar? + emitter.instruction("jz __rt_pcmp_zero"); // two non-scalars are reported equal (documented limitation) + emitter.instruction("test r10, r10"); // is the left operand the non-scalar one? + emitter.instruction("jz __rt_pcmp_pos"); // a non-scalar left operand ranks above a scalar + emitter.instruction("jmp __rt_pcmp_neg"); // a non-scalar right operand ranks above a scalar + + // -- both operands are numbers: promote to double -- + emitter.label("__rt_pcmp_numeric"); + emitter.instruction("cmp rdi, 2"); // is the left operand already a double? + emitter.instruction("je __rt_pcmp_numeric_left_double"); // reinterpret its payload word + emitter.instruction("cvtsi2sd xmm0, rsi"); // widen the left int payload into a double + emitter.instruction("jmp __rt_pcmp_numeric_right"); // continue with the right operand + emitter.label("__rt_pcmp_numeric_left_double"); + emitter.instruction("movq xmm0, rsi"); // reinterpret the left payload word as a double + emitter.label("__rt_pcmp_numeric_right"); + emitter.instruction("cmp rcx, 2"); // is the right operand already a double? + emitter.instruction("je __rt_pcmp_numeric_right_double"); // reinterpret its payload word + emitter.instruction("cvtsi2sd xmm1, r8"); // widen the right int payload into a double + emitter.instruction("jmp __rt_pcmp_fcmp"); // compare both operands as doubles + emitter.label("__rt_pcmp_numeric_right_double"); + emitter.instruction("movq xmm1, r8"); // reinterpret the right payload word as a double + + emitter.label("__rt_pcmp_fcmp"); + emitter.instruction("ucomisd xmm0, xmm1"); // compare both numeric operands as doubles + emitter.instruction("jp __rt_pcmp_pos"); // unordered NaN sorts last, like PHP's three-way compare + emitter.instruction("jb __rt_pcmp_neg"); // an ordered less-than result + emitter.instruction("je __rt_pcmp_zero"); // an ordered equal result + emitter.instruction("jmp __rt_pcmp_pos"); // an ordered greater-than result + + // -- string versus string -- + emitter.label("__rt_pcmp_strings"); + emitter.instruction("mov rax, rsi"); // pass the left string pointer to the numeric parser + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // pass the left string length to the numeric parser + emitter.instruction("call __rt_str_to_number"); // parse the left string under PHP's numeric-string grammar + emitter.instruction("test rax, rax"); // was the left string fully numeric? + emitter.instruction("jz __rt_pcmp_str_bytes"); // a non-numeric operand forces the byte comparison + emitter.instruction("movsd QWORD PTR [rbp - 56], xmm0"); // save the parsed left value across the second parse + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // pass the right string pointer to the numeric parser + emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // pass the right string length to the numeric parser + emitter.instruction("call __rt_str_to_number"); // parse the right string under PHP's numeric-string grammar + emitter.instruction("test rax, rax"); // was the right string fully numeric? + emitter.instruction("jz __rt_pcmp_str_bytes"); // a non-numeric operand forces the byte comparison + emitter.instruction("movapd xmm1, xmm0"); // move the parsed right value into the comparison register + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 56]"); // reload the parsed left value + emitter.instruction("jmp __rt_pcmp_fcmp"); // two numeric strings compare numerically + emitter.label("__rt_pcmp_str_bytes"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the left string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the left string length + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // reload the right string pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the right string length + emitter.instruction("call __rt_strcmp"); // compare both strings byte-wise, then by length + emitter.instruction("cmp rax, 0"); // normalize the byte difference into a three-way result + emitter.instruction("jl __rt_pcmp_neg"); // the left string sorts first + emitter.instruction("jg __rt_pcmp_pos"); // the right string sorts first + emitter.instruction("jmp __rt_pcmp_zero"); // both strings are byte-identical + + // -- number versus string, normalized so the number is always the left operand -- + emitter.label("__rt_pcmp_num_vs_str"); + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // the operands are already in number/string order + emitter.instruction("mov QWORD PTR [rbp - 72], rdi"); // stage the number's runtime tag + emitter.instruction("mov QWORD PTR [rbp - 80], rsi"); // stage the number's payload word + emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // stage the string's pointer + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // stage the string's length + emitter.instruction("jmp __rt_pcmp_num_str_body"); // run the shared number-versus-string comparison + emitter.label("__rt_pcmp_str_vs_num"); + emitter.instruction("mov QWORD PTR [rbp - 64], 1"); // the string is the left operand, so the result is negated + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // stage the number's runtime tag + emitter.instruction("mov QWORD PTR [rbp - 80], r8"); // stage the number's payload word + emitter.instruction("mov QWORD PTR [rbp - 88], rsi"); // stage the string's pointer + emitter.instruction("mov QWORD PTR [rbp - 96], rdx"); // stage the string's length + + emitter.label("__rt_pcmp_num_str_body"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // pass the string pointer to the numeric parser + emitter.instruction("mov rdx, QWORD PTR [rbp - 96]"); // pass the string length to the numeric parser + emitter.instruction("call __rt_str_to_number"); // parse the string under PHP's numeric-string grammar + emitter.instruction("test rax, rax"); // was the string fully numeric? + emitter.instruction("jz __rt_pcmp_num_str_bytes"); // PHP 8 compares a number with a non-numeric string as strings + emitter.instruction("movsd QWORD PTR [rbp - 56], xmm0"); // save the parsed string value + emitter.instruction("mov r10, QWORD PTR [rbp - 72]"); // reload the number's runtime tag + emitter.instruction("mov r11, QWORD PTR [rbp - 80]"); // reload the number's payload word + emitter.instruction("cmp r10, 2"); // is the number already a double? + emitter.instruction("je __rt_pcmp_num_str_double"); // reinterpret its payload word + emitter.instruction("cvtsi2sd xmm0, r11"); // widen the int payload into a double + emitter.instruction("jmp __rt_pcmp_num_str_cmp"); // compare the number against the parsed string + emitter.label("__rt_pcmp_num_str_double"); + emitter.instruction("movq xmm0, r11"); // reinterpret the payload word as a double + emitter.label("__rt_pcmp_num_str_cmp"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 56]"); // reload the parsed string value + emitter.instruction("ucomisd xmm0, xmm1"); // compare the number against the numeric string + emitter.instruction("jp __rt_pcmp_maybe_pos"); // unordered NaN sorts last, before any swap correction + emitter.instruction("jb __rt_pcmp_maybe_neg"); // the number is smaller, before any swap correction + emitter.instruction("je __rt_pcmp_zero"); // both values are numerically equal + emitter.instruction("jmp __rt_pcmp_maybe_pos"); // the number is larger, before any swap correction + + emitter.label("__rt_pcmp_num_str_bytes"); + abi::emit_symbol_address(emitter, "r10", "_concat_off"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // read the shared concat scratch cursor + emitter.instruction("mov QWORD PTR [rbp - 104], r11"); // save it so the rendered number cannot leak scratch space + emitter.instruction("mov r10, QWORD PTR [rbp - 72]"); // reload the number's runtime tag + emitter.instruction("mov r11, QWORD PTR [rbp - 80]"); // reload the number's payload word + emitter.instruction("cmp r10, 2"); // is the number a double? + emitter.instruction("je __rt_pcmp_num_str_ftoa"); // doubles render through PHP's precision-14 formatter + emitter.instruction("mov rax, r11"); // pass the int payload to the decimal formatter + emitter.instruction("call __rt_itoa"); // render the int exactly the way PHP casts it to string + emitter.instruction("jmp __rt_pcmp_num_str_strcmp"); // compare the rendered number with the string + emitter.label("__rt_pcmp_num_str_ftoa"); + emitter.instruction("movq xmm0, r11"); // reinterpret the payload word as the double to render + emitter.instruction("call __rt_ftoa"); // render the double at PHP's default precision of 14 + emitter.label("__rt_pcmp_num_str_strcmp"); + emitter.instruction("mov rdi, rax"); // move the rendered number pointer into the strcmp argument + emitter.instruction("mov rsi, rdx"); // move the rendered number length into the strcmp argument + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // reload the string pointer for the byte comparison + emitter.instruction("mov rcx, QWORD PTR [rbp - 96]"); // reload the string length for the byte comparison + emitter.instruction("call __rt_strcmp"); // compare the rendered number with the string byte-wise + abi::emit_symbol_address(emitter, "r10", "_concat_off"); + emitter.instruction("mov r11, QWORD PTR [rbp - 104]"); // reload the saved concat scratch cursor + emitter.instruction("mov QWORD PTR [r10], r11"); // release the scratch the rendered number occupied + emitter.instruction("cmp rax, 0"); // normalize the byte difference into a three-way result + emitter.instruction("jl __rt_pcmp_maybe_neg"); // the rendered number sorts first, before any swap correction + emitter.instruction("jg __rt_pcmp_maybe_pos"); // the string sorts first, before any swap correction + emitter.instruction("jmp __rt_pcmp_zero"); // both byte sequences are identical + + emitter.label("__rt_pcmp_maybe_neg"); + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // was the string the original left operand? + emitter.instruction("je __rt_pcmp_neg"); // no swap: keep the normalized result + emitter.instruction("jmp __rt_pcmp_pos"); // swapped operands invert the normalized result + emitter.label("__rt_pcmp_maybe_pos"); + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // was the string the original left operand? + emitter.instruction("je __rt_pcmp_pos"); // no swap: keep the normalized result + emitter.instruction("jmp __rt_pcmp_neg"); // swapped operands invert the normalized result + + emitter.label("__rt_pcmp_neg"); + emitter.instruction("mov rax, -1"); // the left operand sorts before the right one + emitter.instruction("jmp __rt_pcmp_done"); // return the three-way result + emitter.label("__rt_pcmp_pos"); + emitter.instruction("mov rax, 1"); // the left operand sorts after the right one + emitter.instruction("jmp __rt_pcmp_done"); // return the three-way result + emitter.label("__rt_pcmp_zero"); + emitter.instruction("xor eax, eax"); // both operands compare equal + + emitter.label("__rt_pcmp_done"); + emitter.instruction("add rsp, 112"); // release the ordering-comparison frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the three-way comparison result +} diff --git a/src/codegen_support/runtime/data/fixed.rs b/src/codegen_support/runtime/data/fixed.rs index 1fad28ce0a..a69f688292 100644 --- a/src/codegen_support/runtime/data/fixed.rs +++ b/src/codegen_support/runtime/data/fixed.rs @@ -9,6 +9,7 @@ //! - Fixed symbols are cached across compilations, so only target-independent runtime data belongs here. use super::{ + ALLOC_OVERFLOW_MSG, ARRAY_ALLOC_SIZE_MSG, BUFFER_ALLOC_SIZE_MSG, RANGE_SIZE_MSG, DIRNAME_LEVELS_MSG, HASH_COPY_FINALIZED_CTX_MSG, HASH_FINAL_FINALIZED_CTX_MSG, HASH_HMAC_UNKNOWN_ALGO_MSG, HASH_INIT_UNKNOWN_ALGO_MSG, HASH_UNKNOWN_ALGO_MSG, HASH_UPDATE_FINALIZED_CTX_MSG, MB_STRLEN_UNKNOWN_ENCODING_MSG, @@ -17,9 +18,15 @@ use super::{ OB_NTC_G_GET_FLUSH, OB_NTC_NO_CLEAN, OB_NTC_NO_END_CLEAN, OB_NTC_NO_END_FLUSH, OB_NTC_NO_FLUSH, OB_NTC_NO_GET_FLUSH, OB_WARN_BAD_CALLBACK_GENERIC, OB_WARN_BAD_CALLBACK_PREFIX, OB_WARN_BAD_CALLBACK_SUFFIX, - PHP_UNAME_MODE_LEN_MSG, PHP_UNAME_MODE_VALUE_MSG, STR_REPEAT_TIMES_MSG, + PHP_UNAME_MODE_LEN_MSG, PHP_UNAME_MODE_VALUE_MSG, SPRINTF_ARGCOUNT_MSG, + SPRINTF_OVERFLOW_MSG, SPRINTF_UNKNOWN_SPEC_MSG, SPRINTF_WIDTH_MSG, STACK_OVERFLOW_MSG, + STR_REPEAT_TIMES_MSG, }; use super::super::system; +use crate::codegen_support::data_section::comm_directive; +use crate::codegen_support::runtime::strings::{ + B64_DECODE_INVALID, B64_DECODE_SKIP, B64_DECODE_WHITESPACE, +}; use crate::codegen_support::platform::Target; use crate::types::checker::builtins::{ all_supported_builtin_function_names, supported_builtin_function_names_for_profile, @@ -42,52 +49,52 @@ use crate::types::checker::builtins::{ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> String { let mut out = String::new(); out.push_str(".data\n"); - out.push_str(".comm _concat_buf, 65536, 3\n"); - out.push_str(".comm _concat_off, 8, 3\n"); + out.push_str(&comm_directive("_concat_buf", 65536, target)); + out.push_str(&comm_directive("_concat_off", 8, target)); // print_r($value, true) return-mode capture state. _print_r_mode is a flag // (0 = write to stdout, 1 = append to _print_r_buf) consulted by // __rt_stdout_write and __rt_pr_write; _print_r_off tracks the accumulated // byte count; _print_r_buf is the 64 KiB accumulation buffer finalized by // __rt_pr_finish into an owned heap string. Only non-zero during an active // print_r return-mode rendering, so non-print_r output is unaffected. - out.push_str(".comm _print_r_mode, 8, 3\n"); - out.push_str(".comm _print_r_off, 8, 3\n"); - out.push_str(".comm _print_r_buf, 65536, 3\n"); + out.push_str(&comm_directive("_print_r_mode", 8, target)); + out.push_str(&comm_directive("_print_r_off", 8, target)); + out.push_str(&comm_directive("_print_r_buf", 65536, target)); // Output-buffering (ob_*) stack state. _ob_level is the active nesting depth // (0 = no buffering) consulted by __rt_stdout_write and __rt_pr_write before // the terminal write syscall; _ob_ptrs/_ob_lens/_ob_caps are 64-slot parallel // arrays (heap buffer base pointer, used bytes, capacity) indexed by level-1. // Buffers are heap-allocated by __rt_ob_start, grown by __rt_ob_append, and // written to the terminal sink by __rt_ob_flush_all at process exit. - out.push_str(".comm _ob_level, 8, 3\n"); - out.push_str(".comm _ob_ptrs, 512, 3\n"); - out.push_str(".comm _ob_lens, 512, 3\n"); - out.push_str(".comm _ob_caps, 512, 3\n"); + out.push_str(&comm_directive("_ob_level", 8, target)); + out.push_str(&comm_directive("_ob_ptrs", 512, target)); + out.push_str(&comm_directive("_ob_lens", 512, target)); + out.push_str(&comm_directive("_ob_caps", 512, target)); // Per-level output-buffer metadata (parallel to _ob_ptrs, indexed by level-1): // the user-handler invocation stub + env word (stub 0 = default handler; env // is a retained callable-descriptor pointer for AOT handlers or a magician // registry id for eval handlers), the persisted handler display name // (ptr/len), the auto-flush chunk size, the ob_start() flags word, and the // started flag (set at the first handler invocation; feeds PHP started bits). - out.push_str(".comm _ob_handler_stubs, 512, 3\n"); - out.push_str(".comm _ob_handler_envs, 512, 3\n"); - out.push_str(".comm _ob_name_ptrs, 512, 3\n"); - out.push_str(".comm _ob_name_lens, 512, 3\n"); - out.push_str(".comm _ob_chunk_sizes, 512, 3\n"); - out.push_str(".comm _ob_flags, 512, 3\n"); - out.push_str(".comm _ob_started, 512, 3\n"); + out.push_str(&comm_directive("_ob_handler_stubs", 512, target)); + out.push_str(&comm_directive("_ob_handler_envs", 512, target)); + out.push_str(&comm_directive("_ob_name_ptrs", 512, target)); + out.push_str(&comm_directive("_ob_name_lens", 512, target)); + out.push_str(&comm_directive("_ob_chunk_sizes", 512, target)); + out.push_str(&comm_directive("_ob_flags", 512, target)); + out.push_str(&comm_directive("_ob_started", 512, target)); // _ob_in_handler: non-zero while a user output handler runs. Output produced // inside a handler is discarded (PHP behavior) via the __rt_stdout_write and // __rt_pr_write branches, and ob_start() inside a handler is a fatal error. - out.push_str(".comm _ob_in_handler, 8, 3\n"); + out.push_str(&comm_directive("_ob_in_handler", 8, target)); // _ob_flushing: re-entry guard for the process-exit drain. A user handler // running during __rt_ob_flush_all may call exit() again; the guard makes // the nested drain a no-op instead of an infinite loop. - out.push_str(".comm _ob_flushing, 8, 3\n"); + out.push_str(&comm_directive("_ob_flushing", 8, target)); // _elephc_eval_ob_handler_fn: installed Rust callback (magician) that runs // an eval-registered ob_start() handler: fn(id, buf, len, phase) -> Mixed // result cell pointer (0 = pass-through). Called via __rt_ob_eval_trampoline. - out.push_str(".comm _elephc_eval_ob_handler_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_eval_ob_handler_fn", 8, target)); // "Closure::__invoke": PHP display name for closure / first-class-callable // output handlers in ob_get_status()/ob_list_handlers(). out.push_str(&format!( @@ -95,7 +102,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin )); // ob_implicit_flush() stored flag. Semantically inert in elephc: terminal // writes are unbuffered syscalls, so implicit flushing is always on. - out.push_str(".comm _ob_implicit_flush, 8, 3\n"); + out.push_str(&comm_directive("_ob_implicit_flush", 8, target)); // ob_get_status()/ob_list_handlers() string constants: PHP's default handler // name and the status-array key strings read by __rt_ob_get_status. out.push_str(&format!( @@ -148,19 +155,19 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // a registry of created value boxes indexed by the same pre-order counter so r: // resolves to the existing value. Capacity bounds the per-call object/value count; // overflow degrades gracefully (serialize stops deduping, unserialize fails the ref). - out.push_str(".comm _ser_value_counter, 8, 3\n"); - out.push_str(".comm _ser_obj_count, 8, 3\n"); - out.push_str(".comm _ser_obj_ptrs, 524288, 3\n"); - out.push_str(".comm _ser_obj_idxs, 524288, 3\n"); - out.push_str(".comm _unser_count, 8, 3\n"); - out.push_str(".comm _unser_values, 524288, 3\n"); - out.push_str(".comm _strtotime_clock, 8, 3\n"); + out.push_str(&comm_directive("_ser_value_counter", 8, target)); + out.push_str(&comm_directive("_ser_obj_count", 8, target)); + out.push_str(&comm_directive("_ser_obj_ptrs", 524288, target)); + out.push_str(&comm_directive("_ser_obj_idxs", 524288, target)); + out.push_str(&comm_directive("_unser_count", 8, target)); + out.push_str(&comm_directive("_unser_values", 524288, target)); + out.push_str(&comm_directive("_strtotime_clock", 8, target)); // Default-timezone state: the "TZ=" env buffer (kept alive for putenv), the stored // identifier length (0 = none set → date_default_timezone_get returns "UTC"), and the // "UTC" literal returned in that default case. - out.push_str(".comm _php_tz_env, 264, 3\n"); - out.push_str(".comm _php_default_tz_len, 8, 3\n"); - out.push_str(".comm _php_tz_save, 264, 3\n"); + out.push_str(&comm_directive("_php_tz_env", 264, target)); + out.push_str(&comm_directive("_php_default_tz_len", 8, target)); + out.push_str(&comm_directive("_php_tz_save", 264, target)); out.push_str(".globl _php_tz_utc\n"); out.push_str("_php_tz_utc:\n"); out.push_str(" .ascii \"UTC\"\n"); @@ -186,17 +193,26 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin ] { out.push_str(&format!(".globl _lt_k_{key}\n_lt_k_{key}:\n .ascii \"{key}\"\n")); } - out.push_str(".comm _global_argc, 8, 3\n"); - out.push_str(".comm _global_argv, 8, 3\n"); - out.push_str(".comm _exc_handler_top, 8, 3\n"); - out.push_str(".comm _exc_call_frame_top, 8, 3\n"); - out.push_str(".comm _exc_value, 8, 3\n"); - out.push_str(".comm _fiber_current, 8, 3\n"); - out.push_str(".comm _fiber_main_saved_sp, 8, 3\n"); - out.push_str(".comm _fiber_main_saved_exc, 8, 3\n"); - out.push_str(".comm _fiber_main_saved_call_frame, 8, 3\n"); - out.push_str(".comm _elephc_eval_dynamic_object_destruct_fn, 8, 3\n"); - out.push_str(".comm _rt_diag_suppression, 8, 3\n"); + out.push_str(&comm_directive("_global_argc", 8, target)); + out.push_str(&comm_directive("_global_argv", 8, target)); + out.push_str(&comm_directive("_exc_handler_top", 8, target)); + out.push_str(&comm_directive("_exc_call_frame_top", 8, target)); + out.push_str(&comm_directive("_exc_value", 8, target)); + out.push_str(&comm_directive("_fiber_current", 8, target)); + out.push_str(&comm_directive("_fiber_main_saved_sp", 8, target)); + out.push_str(&comm_directive("_fiber_main_saved_exc", 8, target)); + out.push_str(&comm_directive("_fiber_main_saved_call_frame", 8, target)); + // Call-stack overflow guard state. _stack_limit is the low-water stack address of the + // execution context that is running right now: every compiled function prologue does an + // unsigned compare of the stack pointer against it and branches to __rt_stack_overflow + // when it is below. Zero (the .comm default) disables the guard, so a program that never + // runs __rt_stack_limit_init keeps the pre-guard behavior. _stack_limit_main remembers + // the OS-thread floor so __rt_fiber_switch can restore it when control leaves a fiber + // stack; while a fiber runs, _stack_limit holds that fiber's own floor instead. + out.push_str(&comm_directive("_stack_limit", 8, target)); + out.push_str(&comm_directive("_stack_limit_main", 8, target)); + out.push_str(&comm_directive("_elephc_eval_dynamic_object_destruct_fn", 8, target)); + out.push_str(&comm_directive("_rt_diag_suppression", 8, target)); // elephc_web_capture: per-request output-capture mode flag read by // __rt_stdout_write. Zero (the default) routes echo output to the plain // write(1, …) syscall; non-zero (set only by the --web bridge) routes it to @@ -204,16 +220,17 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // low byte is used, but the 8-byte/align-3 house style keeps it word-aligned. // The symbol is mangled per-target so the bridge's `extern "C"` declaration // resolves to it on every platform (see emit_runtime_data_fixed docs). - out.push_str(&format!( - ".comm {}, 8, 3\n", - target.extern_symbol("elephc_web_capture") + out.push_str(&comm_directive( + &target.extern_symbol("elephc_web_capture"), + 8, + target, )); - out.push_str(&format!(".comm _heap_buf, {}, 3\n", heap_size)); - out.push_str(".comm _heap_off, 8, 3\n"); - out.push_str(".comm _heap_free_list, 8, 3\n"); - out.push_str(".comm _heap_small_bins, 32, 3\n"); - out.push_str(".comm _heap_debug_enabled, 8, 3\n"); - out.push_str(".comm _web_heap_guard_enabled, 8, 3\n"); + out.push_str(&comm_directive("_heap_buf", heap_size, target)); + out.push_str(&comm_directive("_heap_off", 8, target)); + out.push_str(&comm_directive("_heap_free_list", 8, target)); + out.push_str(&comm_directive("_heap_small_bins", 32, target)); + out.push_str(&comm_directive("_heap_debug_enabled", 8, target)); + out.push_str(&comm_directive("_web_heap_guard_enabled", 8, target)); // PHP object-handle pool. `_obj_handle_index` is a DIRECT-MAPPED side table // holding one u32 handle per 16-byte granule of `_heap_buf`: two live heap // blocks can never share a granule because the smallest block is 16 header @@ -224,15 +241,17 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // released handles php-src reuses from; its depth can never exceed the number // of distinct handles, which is bounded by the peak live-object count, which is // bounded by `heap_size / 24`. See `runtime::objects::handles`. - out.push_str(&format!( - ".comm _obj_handle_index, {}, 3\n", - crate::codegen_support::runtime::object_handle_index_slots(heap_size) * 4 + out.push_str(&comm_directive( + "_obj_handle_index", + crate::codegen_support::runtime::object_handle_index_slots(heap_size) * 4, + target, )); - out.push_str(&format!( - ".comm _obj_handle_free, {}, 3\n", - crate::codegen_support::runtime::object_handle_free_slots(heap_size) * 4 + out.push_str(&comm_directive( + "_obj_handle_free", + crate::codegen_support::runtime::object_handle_free_slots(heap_size) * 4, + target, )); - out.push_str(".comm _obj_handle_free_top, 8, 3\n"); + out.push_str(&comm_directive("_obj_handle_free_top", 8, target)); // PHP RESOURCE ids. A SEPARATE numbering space from the object handles above — // php-src keeps `zend_resource.handle` and `zend_object.handle` in two unrelated // lists, so `resource(5)` and `object(C)#5` can and do coexist. The table maps a @@ -242,29 +261,31 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // resource payloads are not heap-block addresses: descriptors are tiny integers // and bridge handles come from the C allocator, so neither has a granule to index. // Occupancy lives in the value word (id 0 = empty slot; minted ids start at 5). - out.push_str(&format!( - ".comm _resource_id_keys, {}, 3\n", - crate::codegen_support::runtime::RESOURCE_ID_TABLE_SLOTS * 8 + out.push_str(&comm_directive( + "_resource_id_keys", + crate::codegen_support::runtime::RESOURCE_ID_TABLE_SLOTS * 8, + target, )); - out.push_str(&format!( - ".comm _resource_id_vals, {}, 3\n", - crate::codegen_support::runtime::RESOURCE_ID_TABLE_SLOTS * 8 + out.push_str(&comm_directive( + "_resource_id_vals", + crate::codegen_support::runtime::RESOURCE_ID_TABLE_SLOTS * 8, + target, )); - out.push_str(".comm _gc_collecting, 8, 3\n"); - out.push_str(".comm _gc_release_suppressed, 8, 3\n"); - out.push_str(".comm _json_last_error, 8, 3\n"); - out.push_str(".comm _json_active_flags, 8, 3\n"); - out.push_str(".comm _json_active_depth, 8, 3\n"); - out.push_str(".comm _json_indent_depth, 8, 3\n"); - out.push_str(".comm _json_depth_limit, 8, 3\n"); - out.push_str(".comm _json_validate_idx, 8, 3\n"); - out.push_str(".comm _json_validate_ptr, 8, 3\n"); - out.push_str(".comm _json_validate_len, 8, 3\n"); - out.push_str(".comm _json_decode_assoc, 8, 3\n"); - out.push_str(".comm _json_error_source_ptr, 8, 3\n"); - out.push_str(".comm _json_error_location_active, 8, 3\n"); - out.push_str(".comm _json_error_line, 8, 3\n"); - out.push_str(".comm _json_error_column, 8, 3\n"); + out.push_str(&comm_directive("_gc_collecting", 8, target)); + out.push_str(&comm_directive("_gc_release_suppressed", 8, target)); + out.push_str(&comm_directive("_json_last_error", 8, target)); + out.push_str(&comm_directive("_json_active_flags", 8, target)); + out.push_str(&comm_directive("_json_active_depth", 8, target)); + out.push_str(&comm_directive("_json_indent_depth", 8, target)); + out.push_str(&comm_directive("_json_depth_limit", 8, target)); + out.push_str(&comm_directive("_json_validate_idx", 8, target)); + out.push_str(&comm_directive("_json_validate_ptr", 8, target)); + out.push_str(&comm_directive("_json_validate_len", 8, target)); + out.push_str(&comm_directive("_json_decode_assoc", 8, target)); + out.push_str(&comm_directive("_json_error_source_ptr", 8, target)); + out.push_str(&comm_directive("_json_error_location_active", 8, target)); + out.push_str(&comm_directive("_json_error_line", 8, target)); + out.push_str(&comm_directive("_json_error_column", 8, target)); // `_obj_handle_next` is the never-used PHP object-handle cursor. PHP's first // object is `#1`, so the pool starts at 1 and handle 0 is reserved to mean // "this block never acquired a handle". @@ -282,7 +303,22 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _heap_dbg_bad_refcount_msg\n_heap_dbg_bad_refcount_msg:\n .ascii \"Fatal error: heap debug detected bad refcount\\n\"\n"); out.push_str(".globl _heap_dbg_double_free_msg\n_heap_dbg_double_free_msg:\n .ascii \"Fatal error: heap debug detected double free\\n\"\n"); out.push_str(".globl _heap_dbg_free_list_msg\n_heap_dbg_free_list_msg:\n .ascii \"Fatal error: heap debug detected free-list corruption\\n\"\n"); - out.push_str(".globl _arr_cap_err_msg\n_arr_cap_err_msg:\n .ascii \"Fatal error: array capacity exceeded\\n\"\n"); + out.push_str(&format!( + ".globl _stack_err_msg\n_stack_err_msg:\n .ascii {:?}\n", + STACK_OVERFLOW_MSG + )); + out.push_str(&format!( + ".globl _arr_cap_err_msg\n_arr_cap_err_msg:\n .ascii {:?}\n", + ARRAY_ALLOC_SIZE_MSG + )); + out.push_str(&format!( + ".globl _range_size_err_msg\n_range_size_err_msg:\n .ascii {:?}\n", + RANGE_SIZE_MSG + )); + out.push_str(&format!( + ".globl _buffer_alloc_size_msg\n_buffer_alloc_size_msg:\n .ascii {:?}\n", + BUFFER_ALLOC_SIZE_MSG + )); out.push_str(".globl _buffer_bounds_msg\n_buffer_bounds_msg:\n .ascii \"Fatal error: buffer index out of bounds\\n\"\n"); out.push_str(".globl _buffer_uaf_msg\n_buffer_uaf_msg:\n .ascii \"Fatal error: use of buffer after buffer_free()\\n\"\n"); out.push_str(".globl _closure_bind_unsupported_msg\n_closure_bind_unsupported_msg:\n .ascii \"Fatal error: Closure::bind requires a closure that captures only $this\\n\"\n"); @@ -292,10 +328,30 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _static_prop_private_access_msg\n_static_prop_private_access_msg:\n .ascii \"Fatal error: Cannot access private static property\\n\"\n"); out.push_str(".globl _ptr_null_err_msg\n_ptr_null_err_msg:\n .ascii \"Fatal error: null pointer dereference\\n\"\n"); out.push_str(".globl _ptr_read_string_len_err_msg\n_ptr_read_string_len_err_msg:\n .ascii \"Fatal error: ptr_read_string() length must be non-negative\\n\"\n"); + out.push_str(&format!( + ".globl _alloc_overflow_msg\n_alloc_overflow_msg:\n .ascii {:?}\n", + ALLOC_OVERFLOW_MSG + )); out.push_str(&format!( ".globl _str_repeat_times_msg\n_str_repeat_times_msg:\n .ascii {:?}\n", STR_REPEAT_TIMES_MSG )); + out.push_str(&format!( + ".globl _sprintf_width_msg\n_sprintf_width_msg:\n .ascii {:?}\n", + SPRINTF_WIDTH_MSG + )); + out.push_str(&format!( + ".globl _sprintf_overflow_msg\n_sprintf_overflow_msg:\n .ascii {:?}\n", + SPRINTF_OVERFLOW_MSG + )); + out.push_str(&format!( + ".globl _sprintf_argcount_msg\n_sprintf_argcount_msg:\n .ascii {:?}\n", + SPRINTF_ARGCOUNT_MSG + )); + out.push_str(&format!( + ".globl _sprintf_unknown_spec_msg\n_sprintf_unknown_spec_msg:\n .ascii {:?}\n", + SPRINTF_UNKNOWN_SPEC_MSG + )); out.push_str(&format!( ".globl _hash_unknown_algo_msg\n_hash_unknown_algo_msg:\n .ascii {:?}\n", HASH_UNKNOWN_ALGO_MSG @@ -421,6 +477,14 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _instanceof_target_type_msg\n_instanceof_target_type_msg:\n .ascii \"Fatal error: Class name must be a valid object or a string\\n\"\n"); out.push_str(".globl _diag_file_get_contents_failed_msg\n_diag_file_get_contents_failed_msg:\n .ascii \"Warning: file_get_contents(): Failed to open stream\\n\"\n"); out.push_str(".globl _diag_fopen_failed_msg\n_diag_fopen_failed_msg:\n .ascii \"Warning: fopen(): Failed to open stream\\n\"\n"); + // -- php-src's unreachable-seek warning fragments, shared with `__rt_file_get_contents_range` -- + // The helper derives its `__rt_concat` length immediates from the same table, so the bytes + // here and the immediates there can never drift apart. + for (label, message) in + crate::codegen_support::runtime::io::FILE_GET_CONTENTS_SEEK_MESSAGES + { + out.push_str(&format!(".globl {label}\n{label}:\n .ascii {message:?}\n")); + } out.push_str(".globl _diag_define_already_defined_msg\n_diag_define_already_defined_msg:\n .ascii \"Warning: define(): Constant already defined\\n\"\n"); out.push_str(".globl _diag_undefined_array_key_prefix\n_diag_undefined_array_key_prefix:\n .ascii \"Warning: Undefined array key \"\n"); out.push_str(".globl _diag_undefined_array_key_quote\n_diag_undefined_array_key_quote:\n .ascii \"\\\"\"\n"); @@ -440,6 +504,14 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin for (label, message) in crate::codegen_support::runtime::arrays::ARRAY_FLIP_SKIPPED_MESSAGES { out.push_str(&format!(".globl {label}\n{label}:\n .ascii {message:?}\n")); } + // -- php-src's array_count_values() skipped-entry warning, shared with its runtime emitter -- + // The emitter derives its `write()` length from the same table, so the bytes here and the + // immediate there can never drift apart. + for (label, message) in + crate::codegen_support::runtime::arrays::ARRAY_COUNT_VALUES_SKIPPED_MESSAGES + { + out.push_str(&format!(".globl {label}\n{label}:\n .ascii {message:?}\n")); + } // -- PHP 8.5's NAN-to-bool coercion warning, shared with `__rt_warn_nan_coerced_bool` -- // Emitted for every profile even though only 8.5 call sites reference it: the literal is // 50 bytes of `.data` and keeping it unconditional means the runtime `.data` layout does @@ -456,120 +528,120 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _fiber_msg_suspend_outside\n_fiber_msg_suspend_outside:\n .ascii \"Cannot suspend outside of a fiber\"\n"); out.push_str(".globl _fiber_msg_unsupported_callable\n_fiber_msg_unsupported_callable:\n .ascii \"Fiber callable is not supported by this compiler\"\n"); out.push_str(".globl _fiber_msg_stack_alloc_failed\n_fiber_msg_stack_alloc_failed:\n .ascii \"Cannot allocate fiber stack\"\n"); - out.push_str(&emit_builtin_callable_data()); - out.push_str(".comm _gc_allocs, 8, 3\n"); - out.push_str(".comm _gc_frees, 8, 3\n"); - out.push_str(".comm _gc_live, 8, 3\n"); - out.push_str(".comm _gc_peak, 8, 3\n"); - out.push_str(".comm _cstr_buf, 4096, 3\n"); - out.push_str(".comm _cstr_buf2, 4096, 3\n"); - out.push_str(".comm _eof_flags, 256, 3\n"); - out.push_str(".comm _popen_files, 2048, 3\n"); - out.push_str(".comm _dir_handles, 2048, 3\n"); + out.push_str(&emit_builtin_callable_data(target)); + out.push_str(&comm_directive("_gc_allocs", 8, target)); + out.push_str(&comm_directive("_gc_frees", 8, target)); + out.push_str(&comm_directive("_gc_live", 8, target)); + out.push_str(&comm_directive("_gc_peak", 8, target)); + out.push_str(&comm_directive("_cstr_buf", 4096, target)); + out.push_str(&comm_directive("_cstr_buf2", 4096, target)); + out.push_str(&comm_directive("_eof_flags", 256, target)); + out.push_str(&comm_directive("_popen_files", 2048, target)); + out.push_str(&comm_directive("_dir_handles", 2048, target)); // Per-fd glob:// state pointers (256 fds × 8B). Each slot is a pointer to // a heap-allocated glob_state struct (pathv ptr + pathc + index + the // libc glob_t whose lifetime globfree() needs at closedir time). The // readdir/closedir/rewinddir helpers probe this table first; a non-zero // entry routes them through the glob iterator instead of the libc DIR*. - out.push_str(".comm _glob_handles, 2048, 3\n"); - out.push_str(".comm _stream_read_filters, 256, 3\n"); - out.push_str(".comm _stream_write_filters, 256, 3\n"); - out.push_str(".comm _stream_filter_buf, 65536, 3\n"); + out.push_str(&comm_directive("_glob_handles", 2048, target)); + out.push_str(&comm_directive("_stream_read_filters", 256, target)); + out.push_str(&comm_directive("_stream_write_filters", 256, target)); + out.push_str(&comm_directive("_stream_filter_buf", 65536, target)); // 64KB scratch used by length-growing stream filters (convert.base64-encode, // convert.quoted-printable-encode). The filter encodes into the scratch and // then memcpy()s back into the caller's buffer, capping input at 49152 bytes // so the 4/3 base64 expansion still fits the scratch. - out.push_str(".comm _stream_grow_scratch, 65536, 3\n"); - out.push_str(".comm _zstream_handles, 2048, 3\n"); - out.push_str(".comm _zlib_fwrite_fn, 8, 3\n"); - out.push_str(".comm _zlib_close_fn, 8, 3\n"); - out.push_str(".comm _phar_zlib_inflate_init2_fn, 8, 3\n"); - out.push_str(".comm _phar_zlib_inflate_fn, 8, 3\n"); - out.push_str(".comm _phar_zlib_inflate_end_fn, 8, 3\n"); + out.push_str(&comm_directive("_stream_grow_scratch", 65536, target)); + out.push_str(&comm_directive("_zstream_handles", 2048, target)); + out.push_str(&comm_directive("_zlib_fwrite_fn", 8, target)); + out.push_str(&comm_directive("_zlib_close_fn", 8, target)); + out.push_str(&comm_directive("_phar_zlib_inflate_init2_fn", 8, target)); + out.push_str(&comm_directive("_phar_zlib_inflate_fn", 8, target)); + out.push_str(&comm_directive("_phar_zlib_inflate_end_fn", 8, target)); out.push_str(".globl _zlib_version\n_zlib_version:\n .asciz \"1\"\n"); // bzip2.compress write-filter state: per-fd bz_stream pointer table // (_bzstream_handles, indexed by fd) plus the indirect fn-pointer slots the // shared runtime calls through so non-bzip2 programs never link -lbz2. - out.push_str(".comm _bzstream_handles, 2048, 3\n"); - out.push_str(".comm _bz2_fwrite_fn, 8, 3\n"); - out.push_str(".comm _bz2_close_fn, 8, 3\n"); - out.push_str(".comm _phar_bz2_decompress_fn, 8, 3\n"); + out.push_str(&comm_directive("_bzstream_handles", 2048, target)); + out.push_str(&comm_directive("_bz2_fwrite_fn", 8, target)); + out.push_str(&comm_directive("_bz2_close_fn", 8, target)); + out.push_str(&comm_directive("_phar_bz2_decompress_fn", 8, target)); // convert.iconv.* WRITE-filter state: per-fd iconv_t descriptor table // (_iconv_handles) plus the indirect fn-pointer slots the shared runtime // calls through so it never names libc iconv (which needs -liconv on macOS). - out.push_str(".comm _iconv_handles, 2048, 3\n"); - out.push_str(".comm _iconv_fwrite_fn, 8, 3\n"); - out.push_str(".comm _iconv_close_fn, 8, 3\n"); - out.push_str(".comm _ftp_resp_buf, 4096, 3\n"); - out.push_str(".comm _ftp_data_addr, 64, 3\n"); + out.push_str(&comm_directive("_iconv_handles", 2048, target)); + out.push_str(&comm_directive("_iconv_fwrite_fn", 8, target)); + out.push_str(&comm_directive("_iconv_close_fn", 8, target)); + out.push_str(&comm_directive("_ftp_resp_buf", 4096, target)); + out.push_str(&comm_directive("_ftp_data_addr", 64, target)); // _ftp_use_tls: set to 1 by fopen("ftps://...") before __rt_ftp_open is // invoked. The handshake helper interprets it as "perform AUTH TLS on the // control connection, PBSZ 0 + PROT P after USER/PASS, and elephc-tls- // attach the PASV data connection". Reset to 0 at the end of __rt_ftp_open // so subsequent plain ftp:// opens are not contaminated. - out.push_str(".comm _ftp_use_tls, 8, 3\n"); - out.push_str(".comm _http_resp_buf, 1048576, 3\n"); + out.push_str(&comm_directive("_ftp_use_tls", 8, target)); + out.push_str(&comm_directive("_http_resp_buf", 1048576, target)); // https:// goes through indirect function pointers so only programs that // actually open https URLs reference elephc-tls (and pull in -lelephc_tls // at link time); other programs keep the runtime libc-only. - out.push_str(".comm _elephc_tls_connect_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_connect_fn", 8, target)); // _elephc_tls_connect_insecure_fn: same shape as _elephc_tls_connect_fn // but dispatched when the caller has set ssl.verify_peer = false on the // stream context. The runtime picks one over the other at https_open // time so non-TLS programs still don't link elephc-tls. - out.push_str(".comm _elephc_tls_connect_insecure_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_connect_insecure_fn", 8, target)); // _elephc_tls_connect_cafile_fn: dispatched when the caller has set // ssl.cafile on the stream context. Same late-binding pattern; takes two // extra args (cafile path ptr/len) that the secure/insecure variants ignore. - out.push_str(".comm _elephc_tls_connect_cafile_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_connect_cafile_fn", 8, target)); // _elephc_tls_connect_capath_fn / _peer_name_fn: dispatched for ssl.capath // (a directory of CA certs) and ssl.peer_name (verify the cert for a name // other than the connection host). Same late-binding/extra-args pattern. - out.push_str(".comm _elephc_tls_connect_capath_fn, 8, 3\n"); - out.push_str(".comm _elephc_tls_connect_peer_name_fn, 8, 3\n"); - out.push_str(".comm _elephc_tls_write_fn, 8, 3\n"); - out.push_str(".comm _elephc_tls_read_fn, 8, 3\n"); - out.push_str(".comm _elephc_tls_close_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_connect_capath_fn", 8, target)); + out.push_str(&comm_directive("_elephc_tls_connect_peer_name_fn", 8, target)); + out.push_str(&comm_directive("_elephc_tls_write_fn", 8, target)); + out.push_str(&comm_directive("_elephc_tls_read_fn", 8, target)); + out.push_str(&comm_directive("_elephc_tls_close_fn", 8, target)); // _elephc_tls_attach_fd_fn: indirect pointer to elephc_tls_attach_fd, // used by stream_socket_enable_crypto to promote an existing TCP fd to // a TLS session without re-establishing the TCP connection. Same // late-binding pattern as the other tls fn slots so non-TLS programs // do not pull in elephc-tls at link time. - out.push_str(".comm _elephc_tls_attach_fd_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_attach_fd_fn", 8, target)); // _elephc_tls_attach_fd_client_cert_fn / _elephc_tls_connect_client_cert_fn: // mutual-TLS variants dispatched when the stream context carries both // ssl.local_cert and ssl.local_pk. The attach variant is used by // stream_socket_enable_crypto; both take the extra cert/key path ptr/len // pairs that the non-client-cert variants ignore. Same late-binding pattern. - out.push_str(".comm _elephc_tls_attach_fd_client_cert_fn, 8, 3\n"); - out.push_str(".comm _elephc_tls_connect_client_cert_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_tls_attach_fd_client_cert_fn", 8, target)); + out.push_str(&comm_directive("_elephc_tls_connect_client_cert_fn", 8, target)); // _elephc_crypto_hash_fn: indirect pointer to elephc_crypto_hash, published // only at a hash() call site so the shared runtime __rt_hash can call through // it without the runtime itself naming elephc-crypto. Programs that never // call hash() leave the slot null and do not pull in -lelephc_crypto. - out.push_str(".comm _elephc_crypto_hash_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_crypto_hash_fn", 8, target)); // _elephc_crypto_hmac_fn: indirect pointer to elephc_crypto_hmac, published // only at a hash_hmac() call site so the shared runtime __rt_hash_hmac can call // through it without the runtime itself naming elephc-crypto. Programs that never // call hash_hmac() leave the slot null and do not pull in -lelephc_crypto. - out.push_str(".comm _elephc_crypto_hmac_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_crypto_hmac_fn", 8, target)); // Incremental HashContext entry slots, published by hash_init/update/final/copy. - out.push_str(".comm _elephc_crypto_init_fn, 8, 3\n"); - out.push_str(".comm _elephc_crypto_update_fn, 8, 3\n"); - out.push_str(".comm _elephc_crypto_final_fn, 8, 3\n"); - out.push_str(".comm _elephc_crypto_clone_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_crypto_init_fn", 8, target)); + out.push_str(&comm_directive("_elephc_crypto_update_fn", 8, target)); + out.push_str(&comm_directive("_elephc_crypto_final_fn", 8, target)); + out.push_str(&comm_directive("_elephc_crypto_clone_fn", 8, target)); // _elephc_crypto_free_fn: indirect pointer to elephc_crypto_free, published // at hash_init/hash_copy call sites and used by __rt_hash_ctx_free so the // shared runtime can release unfinalized HashContext handles without naming // elephc-crypto directly. - out.push_str(".comm _elephc_crypto_free_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_crypto_free_fn", 8, target)); // _elephc_crypto_is_finalized_fn: indirect pointer to elephc_crypto_is_finalized. // __rt_hash_update / __rt_hash_final / __rt_hash_copy ask through it whether the // incoming context was already consumed by a previous hash_final(), which is the // condition PHP 8 answers with a TypeError. A null slot means the bridge is not // linked, in which case the guards skip the question exactly like every other // elephc-crypto call in this family. - out.push_str(".comm _elephc_crypto_is_finalized_fn, 8, 3\n"); + out.push_str(&comm_directive("_elephc_crypto_is_finalized_fn", 8, target)); // _elephc_phar_extract_url_fn: indirect pointer to the elephc-phar bridge // reader. Dynamic phar:// paths publish it before calling the runtime // reader; literal phar:// paths are still decoded at compile time. @@ -631,24 +703,24 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // fd up to 256; the runtime fread/fwrite/fclose paths consult this // table and route through the elephc-tls helpers when an entry is // non-zero, falling back to read/write/close syscalls otherwise. - out.push_str(".comm _tls_sessions, 2048, 3\n"); + out.push_str(&comm_directive("_tls_sessions", 2048, target)); // _stream_chunk_size: per-fd read/write chunk size set by // stream_set_chunk_size, indexed by raw fd up to 256 (8 bytes each). A zero // entry means "unset" and reports PHP's default of 8192. stream_set_chunk_size // returns the previous value (the PHP-observable contract); the size does not // currently change read granularity (reads return identical data). - out.push_str(".comm _stream_chunk_size, 2048, 3\n"); + out.push_str(&comm_directive("_stream_chunk_size", 2048, target)); // _stream_connect_host: per-fd transport host string (ptr, len) captured by // stream_socket_client so stream_socket_enable_crypto can default the TLS // SNI / peer-name to the connection host when no ssl.peer_name context // option is set. 256 fds * 16 bytes (ptr + len). A zero len means "unset". - out.push_str(".comm _stream_connect_host, 4096, 3\n"); + out.push_str(&comm_directive("_stream_connect_host", 4096, target)); // _stream_notification_callback: the callable descriptor pointer for the // stream context's `notification` option, captured at codegen time by // stream_context_create / stream_context_set_params. __rt_http_open fires // it at the CONNECT, COMPLETED, and FAILURE transfer milestones. Zero when // no notification callback is registered (the fire shim is then a no-op). - out.push_str(".comm _stream_notification_callback, 8, 3\n"); + out.push_str(&comm_directive("_stream_notification_callback", 8, target)); // _tls_peer_name_default: hardcoded peer-name buffer used as the SNI // hint when stream_socket_enable_crypto is called without a context // peer_name. v1 limitation — production TLS needs real peer-name @@ -729,12 +801,12 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // zero-length string null-fallback (out-of-bounds indexed read / assoc miss // on a Str-typed array). len 0 means no bytes are ever read; the valid // pointer keeps any echo/strlen path that still loads the pointer safe. - out.push_str(".comm _empty_str, 1, 1\n"); + out.push_str(&comm_directive("_empty_str", 1, target)); // _url_stat_matched: set to 1 by __rt_user_wrapper_url_stat when a path's // scheme matches a registered userspace wrapper, 0 otherwise. The path-based // stat builtins (file_exists/is_file/filesize) read it after the call to // decide between the wrapper's url_stat() result and the real filesystem. - out.push_str(".comm _url_stat_matched, 1, 1\n"); + out.push_str(&comm_directive("_url_stat_matched", 1, target)); out.push_str( ".globl _socket_key_str\n_socket_key_str:\n .ascii \"socket\"\n", ); @@ -768,25 +840,25 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // 0 = fail-open behavior (default in PHP). // _http_active_max_redirects : count of remaining hops for // follow_location loops (0 disables). - out.push_str(".comm _http_active_ignore_errors, 8, 3\n"); - out.push_str(".comm _http_active_max_redirects, 8, 3\n"); - out.push_str(".comm _http_active_timeout_seconds, 8, 3\n"); + out.push_str(&comm_directive("_http_active_ignore_errors", 8, target)); + out.push_str(&comm_directive("_http_active_max_redirects", 8, target)); + out.push_str(&comm_directive("_http_active_timeout_seconds", 8, target)); // Proxy override for __rt_http_open: when non-zero, used as the TCP // connect target instead of the host extracted from the URL. Value // shape is "tcp://proxyhost:port" — the same format // __rt_stream_socket_client expects. - out.push_str(".comm _http_active_proxy_ptr, 8, 3\n"); - out.push_str(".comm _http_active_proxy_len, 8, 3\n"); + out.push_str(&comm_directive("_http_active_proxy_ptr", 8, target)); + out.push_str(&comm_directive("_http_active_proxy_len", 8, target)); // Host info written by __rt_http_build_request and consumed by // __rt_http_open when [http][follow_location] triggers an internal // redirect — we rebuild the request with the saved host + the // Location-header path. - out.push_str(".comm _http_active_host_ptr, 8, 3\n"); - out.push_str(".comm _http_active_host_len, 8, 3\n"); + out.push_str(&comm_directive("_http_active_host_ptr", 8, target)); + out.push_str(&comm_directive("_http_active_host_len", 8, target)); // 2 KiB scratch for the Location header's path component on // relative redirects (covers the vast majority of API redirects). - out.push_str(".comm _http_redirect_path_buf, 2048, 3\n"); - out.push_str(".comm _http_redirect_path_len, 8, 3\n"); + out.push_str(&comm_directive("_http_redirect_path_buf", 2048, target)); + out.push_str(&comm_directive("_http_redirect_path_len", 8, target)); out.push_str( ".globl _http_request_fulluri_key_str\n_http_request_fulluri_key_str:\n .ascii \"request_fulluri\"\n", ); @@ -820,7 +892,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // PHP int (19 ascii digits) + "REST " (5) + "\r\n" (2) = 26 bytes, so // 64 leaves generous headroom for future extensions (auth, custom // commands). - out.push_str(".comm _ftp_cmd_scratch, 64, 3\n"); + out.push_str(&comm_directive("_ftp_cmd_scratch", 64, target)); // Bucket-brigade property keys used by __rt_user_filter_brigade_invoke // to build and walk brigade-shaped argument data when the user's @@ -848,26 +920,26 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // `__rt_http_build_request` and consumed by `__rt_http_open` via // the http_stream lowering when context options can override the // default method. - out.push_str(".comm _http_req_scratch, 8192, 3\n"); - out.push_str(".comm _fgc_url_addr, 512, 3\n"); - out.push_str(".comm _fgc_url_retr, 2048, 3\n"); + out.push_str(&comm_directive("_http_req_scratch", 8192, target)); + out.push_str(&comm_directive("_fgc_url_addr", 512, target)); + out.push_str(&comm_directive("_fgc_url_retr", 2048, target)); out.push_str(".globl _fgc_url_slash\n_fgc_url_slash:\n .ascii \"/\"\n"); - out.push_str(".comm _https_resp_buf, 1048576, 3\n"); - out.push_str(".comm _fsockopen_addr, 512, 3\n"); + out.push_str(&comm_directive("_https_resp_buf", 1048576, target)); + out.push_str(&comm_directive("_fsockopen_addr", 512, target)); // _user_wrappers: USER_WRAPPER_REGISTRATIONS_CAP = 64 scheme→class // registrations, each entry 32 bytes (protocol_ptr/len + class_ptr/len). // Slot is free when protocol_ptr is null. 64 × 32 = 2048 bytes. - out.push_str(".comm _user_wrappers, 2048, 3\n"); + out.push_str(&comm_directive("_user_wrappers", 2048, target)); // _user_wrapper_handles: USER_WRAPPER_HANDLES_CAP = 256 active stream-handle // slots, each storing the wrapper object pointer keyed by synthetic fd // `USER_WRAPPER_FD_BASE + slot_index`. Slot is free when the stored pointer // is null. 256 slots × 8 bytes = 2048 bytes. - out.push_str(".comm _user_wrapper_handles, 2048, 3\n"); + out.push_str(&comm_directive("_user_wrapper_handles", 2048, target)); // _user_wrapper_drain_buf: 1 MiB accumulation buffer for the codegen-level // feof-gated read loop emitted by stream_get_contents on a wrapper fd. // Each fread chunk is copied here, building one contiguous result. Drains // larger than 1 MiB are truncated (v1). - out.push_str(".comm _user_wrapper_drain_buf, 1048576, 3\n"); + out.push_str(&comm_directive("_user_wrapper_drain_buf", 1048576, target)); // phar:// write stream state. _phar_write_out is the 1 MiB in-memory // payload buffer (template prefix + entry content); _phar_write_len is the // bytes used; _phar_write_tpl_len locates the entry payload. The path and @@ -875,39 +947,39 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // read-modify-write bridge. The url ptr/len pair keeps a runtime-built // phar:// URL alive until fclose() can route it through the URL bridge. // Fallback only: one stream at a time; synthetic fd 0x50000000. - out.push_str(".comm _phar_write_out, 1048576, 3\n"); - out.push_str(".comm _phar_write_len, 8, 3\n"); - out.push_str(".comm _phar_write_tpl_len, 8, 3\n"); - out.push_str(".comm _phar_write_path_ptr, 8, 3\n"); - out.push_str(".comm _phar_write_path_len, 8, 3\n"); - out.push_str(".comm _phar_write_entry_ptr, 8, 3\n"); - out.push_str(".comm _phar_write_entry_len, 8, 3\n"); - out.push_str(".comm _phar_write_url_ptr, 8, 3\n"); - out.push_str(".comm _phar_write_url_len, 8, 3\n"); + out.push_str(&comm_directive("_phar_write_out", 1048576, target)); + out.push_str(&comm_directive("_phar_write_len", 8, target)); + out.push_str(&comm_directive("_phar_write_tpl_len", 8, target)); + out.push_str(&comm_directive("_phar_write_path_ptr", 8, target)); + out.push_str(&comm_directive("_phar_write_path_len", 8, target)); + out.push_str(&comm_directive("_phar_write_entry_ptr", 8, target)); + out.push_str(&comm_directive("_phar_write_entry_len", 8, target)); + out.push_str(&comm_directive("_phar_write_url_ptr", 8, target)); + out.push_str(&comm_directive("_phar_write_url_len", 8, target)); // _stream_open_opened_path_scratch: 16-byte scratch backing the 5th // `?string &$opened_path` parameter of stream_open. The runtime passes // its address so wrappers that follow the PHP-faithful signature can // safely write to it; elephc v1 zeroes the slot before each call and // does not read the value back. - out.push_str(".comm _stream_open_opened_path_scratch, 16, 3\n"); + out.push_str(&comm_directive("_stream_open_opened_path_scratch", 16, target)); // _user_filter_registry: 128 (filter_name, class_name) registrations, // each entry 32 bytes (filter_name_ptr/len + class_name_ptr/len). Slot // is free when filter_name_ptr is null. User filter IDs are slot_index // + USER_FILTER_ID_BASE (128) so they don't collide with the existing // u8 built-in filter IDs (1..=4). 128 × 32 = 4096 bytes. - out.push_str(".comm _user_filter_registry, 4096, 3\n"); + out.push_str(&comm_directive("_user_filter_registry", 4096, target)); // _user_filter_instances: one wrapper-class instance per attached // filter, keyed by (fd, direction). Slot = _user_filter_instances[fd*2 // + dir] where dir=0 is read, dir=1 is write. Slot is null when no // user filter is attached. 256 fds × 2 dirs × 8 B = 4096 bytes. - out.push_str(".comm _user_filter_instances, 4096, 3\n"); + out.push_str(&comm_directive("_user_filter_instances", 4096, target)); // _stream_context_options: pointer to the current stream-context // options hash (nested array of `wrapper => option => value`). // stream_context_create() stores its options arg here; consumers // (http://, ftp://, fopen 4th arg) read it back through // __rt_hash_get. v1 limitation: only one active context at a time — // a fresh stream_context_create overwrites the slot. - out.push_str(".comm _stream_context_options, 8, 3\n"); + out.push_str(&comm_directive("_stream_context_options", 8, target)); // var_dump body literals (rodata): per-element prefix/suffix bytes used by // the array/hash walkers. NONE of them carry a leading indent: every // var_dump line is padded by `__rt_vd_pad`, which writes `_vd_indent` @@ -937,7 +1009,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // _vd_indent: current var_dump line indentation, in spaces. The var_dump // builtin sets it to 2 around a top-level array body and back to 0 after; // `__rt_var_dump_value` bumps it by 2 across each nested container walk. - out.push_str(".comm _vd_indent, 8, 3\n"); + out.push_str(&comm_directive("_vd_indent", 8, target)); // var_dump object delimiters: `object(` + class name + `)#` + the PHP object // handle + ` (` + initialized property count + `) {\n` opens an object on its // value line; the shared `_vd_brace_close` closes it. The handle is the same @@ -957,8 +1029,8 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // object body by `__rt_var_dump_value`'s tag-6 branch. 256 entries × 8 B. // The capacity MUST match `VD_SEEN_CAPACITY` in `runtime::io::var_dump_object`: // a lookup that reaches the cap reports recursion, which is what bounds the walk. - out.push_str(".comm _vd_seen, 2048, 3\n"); - out.push_str(".comm _vd_seen_n, 8, 3\n"); + out.push_str(&comm_directive("_vd_seen", 2048, target)); + out.push_str(&comm_directive("_vd_seen_n", 8, target)); // print_r body literals (rodata): PHP's `Array\n(\n` header, `)\n` footer, // `[`/`] => ` key delimiters (unquoted keys, unlike var_dump), a lone // newline, the `1` rendered for boolean true, and a 64-space pad used by @@ -970,6 +1042,23 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _pr_arrow\n_pr_arrow:\n .ascii \"] => \"\n"); out.push_str(".globl _pr_nl\n_pr_nl:\n .ascii \"\\n\"\n"); out.push_str(".globl _pr_one\n_pr_one:\n .ascii \"1\"\n"); + // print_r object literals: the header suffix PHP writes after the class name + // (`C Object`, and `C Enum` / `C Enum:int` / `C Enum:string` for an enum case), + // and the ` *RECURSION*` marker a revisited instance renders instead of a body. + // The marker deliberately carries NO newline — the entry line terminator is + // written by whichever walker opened the `[key] => ` line, exactly like PHP. + out.push_str(".globl _pr_object_suffix\n_pr_object_suffix:\n .ascii \" Object\\n\"\n"); + out.push_str(".globl _pr_enum_suffix\n_pr_enum_suffix:\n .ascii \" Enum\\n\"\n"); + out.push_str(".globl _pr_enum_int_suffix\n_pr_enum_int_suffix:\n .ascii \" Enum:int\\n\"\n"); + out.push_str(".globl _pr_enum_str_suffix\n_pr_enum_str_suffix:\n .ascii \" Enum:string\\n\"\n"); + out.push_str(".globl _pr_recursion\n_pr_recursion:\n .ascii \" *RECURSION*\"\n"); + // var_dump enum literals: PHP renders an enum case as `enum(Class::Case)` + // instead of an object body, so the three fragments bracket the class name + // (from `_class_name_entries`) and the case name (from the instance's `name` + // property slot). + out.push_str(".globl _vd_enum_prefix\n_vd_enum_prefix:\n .ascii \"enum(\"\n"); + out.push_str(".globl _vd_enum_sep\n_vd_enum_sep:\n .ascii \"::\"\n"); + out.push_str(".globl _vd_enum_close\n_vd_enum_close:\n .ascii \")\\n\"\n"); out.push_str(".globl _pr_spaces\n_pr_spaces:\n .ascii \" \"\n"); out.push_str(".globl _ftp_user_cmd\n_ftp_user_cmd:\n .ascii \"USER anonymous\\x0d\\n\"\n"); out.push_str(".globl _ftp_pass_cmd\n_ftp_pass_cmd:\n .ascii \"PASS anonymous@\\x0d\\n\"\n"); @@ -982,15 +1071,15 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _ftp_auth_tls_cmd\n_ftp_auth_tls_cmd:\n .ascii \"AUTH TLS\\x0d\\n\"\n"); out.push_str(".globl _ftp_pbsz_cmd\n_ftp_pbsz_cmd:\n .ascii \"PBSZ 0\\x0d\\n\"\n"); out.push_str(".globl _ftp_prot_p_cmd\n_ftp_prot_p_cmd:\n .ascii \"PROT P\\x0d\\n\"\n"); - out.push_str(".comm _recvfrom_addr_ptr, 8, 3\n"); - out.push_str(".comm _recvfrom_addr_len, 8, 3\n"); - out.push_str(".comm _accept_peer_ptr, 8, 3\n"); - out.push_str(".comm _accept_peer_len, 8, 3\n"); - out.push_str(".comm _protoent_buf, 32768, 3\n"); + out.push_str(&comm_directive("_recvfrom_addr_ptr", 8, target)); + out.push_str(&comm_directive("_recvfrom_addr_len", 8, target)); + out.push_str(&comm_directive("_accept_peer_ptr", 8, target)); + out.push_str(&comm_directive("_accept_peer_len", 8, target)); + out.push_str(&comm_directive("_protoent_buf", 32768, target)); out.push_str(".globl _etc_protocols_path\n_etc_protocols_path:\n .asciz \"/etc/protocols\"\n"); - out.push_str(".comm _servent_buf, 1048576, 3\n"); + out.push_str(&comm_directive("_servent_buf", 1048576, target)); out.push_str(".globl _etc_services_path\n_etc_services_path:\n .asciz \"/etc/services\"\n"); - out.push_str(".comm _principal_lookup_buf, 4096, 3\n"); + out.push_str(&comm_directive("_principal_lookup_buf", 4096, target)); out.push_str(".globl _etc_passwd_path\n_etc_passwd_path:\n .asciz \"/etc/passwd\"\n"); out.push_str(".globl _etc_group_path\n_etc_group_path:\n .asciz \"/etc/group\"\n"); out.push_str(".globl _principal_lookup_read_mode\n_principal_lookup_read_mode:\n .asciz \"r\"\n"); @@ -1010,16 +1099,33 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _fmt_g\n_fmt_g:\n .asciz \"%.14G\"\n"); out.push_str(".globl _fmt_star_e\n_fmt_star_e:\n .asciz \"%.*e\"\n"); out.push_str(".globl _fmt_star_f\n_fmt_star_f:\n .asciz \"%.*f\"\n"); + // PHP's own default `ucwords()` separator set, `" \t\r\n\f\v"`. It is a byte SET rather + // than a substring, and the backend hands this symbol to `__rt_ucwords` whenever the + // optional `$separators` argument is omitted, so the default and an explicitly written + // `" \t\r\n\f\v"` take exactly the same code path. + out.push_str( + ".globl _ucwords_default_seps\n_ucwords_default_seps:\n .byte 32, 9, 13, 10, 12, 11\n", + ); out.push_str(".globl _b64_encode_tbl\n_b64_encode_tbl:\n .ascii \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n"); out.push_str(".globl _b64_decode_tbl\n_b64_decode_tbl:\n"); - let mut decode_tbl = vec![0u8; 256]; + // php-src's `base64_reverse_table`, transposed from its signed `short` entries onto + // unsigned bytes: an alphabet character keeps its 0-63 sextet value, `-1` (skippable + // whitespace) becomes `B64_DECODE_SKIP`, and `-2` (everything else, including `=`) + // becomes `B64_DECODE_INVALID`. `__rt_base64_decode` needs the two rejection classes + // apart: whitespace is dropped in BOTH modes, while any other stray byte is dropped in + // the lax mode and makes `$strict = true` return `false`. Encoding both as 0 — the old + // table's behavior — is what silently decoded `"SGVs bG8="` to garbage. + let mut decode_tbl = vec![B64_DECODE_INVALID; 256]; for (i, &c) in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" .iter() .enumerate() { decode_tbl[c as usize] = i as u8; } + for &c in B64_DECODE_WHITESPACE { + decode_tbl[c as usize] = B64_DECODE_SKIP; + } out.push_str(" .byte "); for (i, val) in decode_tbl.iter().enumerate() { @@ -1108,7 +1214,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin /// holding the total count, and `_callable_builtin_table` containing /// pointer/length pairs for each builtin. Used by the `is_callable()` runtime /// routine and callable-invoke paths. -fn emit_builtin_callable_data() -> String { +fn emit_builtin_callable_data(target: Target) -> String { let mut out = String::new(); let strict_builtins = supported_builtin_function_names_for_profile(true); let mut builtins = all_supported_builtin_function_names(); @@ -1129,7 +1235,7 @@ fn emit_builtin_callable_data() -> String { ".globl _callable_builtin_strict_count\n_callable_builtin_strict_count:\n", ); out.push_str(&format!(" .quad {}\n", strict_builtins.len())); - out.push_str(".comm _callable_strict_profile, 8, 3\n"); + out.push_str(&comm_directive("_callable_strict_profile", 8, target)); out.push_str(".globl _callable_builtin_table\n_callable_builtin_table:\n"); for (idx, name) in builtins.iter().enumerate() { out.push_str(&format!(" .quad _callable_builtin_name_{}\n", idx)); @@ -1166,3 +1272,64 @@ fn emit_spl_autoload_extensions_data() -> String { out.push_str(&format!(" .quad {}\n", default.len())); out } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Arch, Platform}; + + /// Every common symbol the runtime data section declares must carry an alignment operand + /// the target's own assembler reads as 8 bytes. + /// + /// This is a whole-section sweep rather than a spot check because the failure is silent at + /// assembly time and only shows up at link time, once per program rather than once per + /// symbol: `.comm sym, N, 3` means 8-byte alignment to Mach-O's assembler and 3-byte + /// alignment to GNU as. An ELF build of an under-aligned symbol assembles fine and then + /// fails to link with `relocation truncated to fit: R_AARCH64_LDST64_ABS_LO12_NC`, because + /// that relocation encodes its displacement pre-shifted by 3 and cannot name an address + /// that is not 8-byte aligned. `_stack_limit` took out every linux-aarch64 link this way. + #[test] + fn test_runtime_common_symbols_are_aligned_for_each_object_format() { + for (platform, arch, expected) in [ + (Platform::MacOS, Arch::AArch64, "3"), + (Platform::Linux, Arch::AArch64, "8"), + (Platform::Linux, Arch::X86_64, "8"), + ] { + let target = Target { platform, arch }; + let asm = emit_runtime_data_fixed(8_388_608, target); + + let mut seen = 0usize; + for line in asm.lines().filter(|line| line.starts_with(".comm ")) { + seen += 1; + let alignment = line.rsplit(',').next().unwrap().trim(); + assert_eq!( + alignment, expected, + "{:?}/{:?} emitted `{}`, whose alignment operand is not the {}-spelling \ + that this object format's assembler reads as 8 bytes", + platform, arch, line, expected + ); + } + assert!( + seen > 100, + "expected the fixed runtime data to declare its usual common symbols, saw {}", + seen + ); + } + } + + /// Pins the symbol whose under-alignment broke linux-aarch64 linking, so a future + /// hand-written `.comm` for it cannot regress past the sweep above. + #[test] + fn test_stack_limit_is_eight_byte_aligned_on_elf() { + let asm = emit_runtime_data_fixed( + 8_388_608, + Target { + platform: Platform::Linux, + arch: Arch::AArch64, + }, + ); + + assert!(asm.contains(".comm _stack_limit, 8, 8\n")); + assert!(asm.contains(".comm _stack_limit_main, 8, 8\n")); + } +} diff --git a/src/codegen_support/runtime/data/instanceof.rs b/src/codegen_support/runtime/data/instanceof.rs index e4c90966e9..812e46919a 100644 --- a/src/codegen_support/runtime/data/instanceof.rs +++ b/src/codegen_support/runtime/data/instanceof.rs @@ -102,7 +102,7 @@ pub(super) fn escaped_ascii(value: &str) -> String { /// - `\\` for backslash /// - `\"` for double quote /// - `\NNN` (3-digit octal) for any other byte, including null and high bytes -pub(super) fn escaped_bytes(bytes: &[u8]) -> String { +pub(crate) fn escaped_bytes(bytes: &[u8]) -> String { let mut escaped = String::new(); for &byte in bytes { match byte { diff --git a/src/codegen_support/runtime/data/mod.rs b/src/codegen_support/runtime/data/mod.rs index b56ddda9c2..939d6f99a4 100644 --- a/src/codegen_support/runtime/data/mod.rs +++ b/src/codegen_support/runtime/data/mod.rs @@ -9,7 +9,10 @@ //! - Symbol names and table layouts are link-time ABI shared with generated code and runtime helper labels. mod fixed; -mod instanceof; +/// Also home of `escaped_bytes()`, the crate's single assembler-string escaper: +/// reachable outside this module so non-runtime emitters (`crate::debug_info`) +/// escape quoted directive operands the same way. +pub(crate) mod instanceof; mod user; pub(crate) use fixed::emit_runtime_data_fixed; @@ -76,9 +79,65 @@ pub(crate) const OB_CLOSURE_INVOKE_NAME: &str = "Closure::__invoke"; pub(crate) const DIRNAME_LEVELS_MSG: &str = "Fatal error: dirname(): Argument #2 ($levels) must be greater than or equal to 1\n"; +/// Fatal error message written by `__rt_stack_overflow` when a function prologue finds the +/// stack pointer below `_stack_limit`. PHP 8.3+ reports the same condition as +/// `Fatal error: Uncaught Error: Maximum call stack size of N bytes +/// (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?`; +/// elephc has no per-call-site context in the fatal path (it is entered with almost no +/// stack left), so it reports the same condition without the byte count and location. +pub(crate) const STACK_OVERFLOW_MSG: &str = + "Fatal error: Maximum call stack size reached. Infinite recursion?\n"; +/// Fatal error message when an array allocation request cannot be sized safely, +/// i.e. `capacity * elem_size` does not fit in the machine word. PHP reports the +/// same class of failure as a `ValueError` naming the offending argument +/// (`array_fill(): Argument #2 ($count) is too large`); elephc's runtime has no +/// per-call-site context inside `__rt_array_new`, so it reports the shared cause. +pub(crate) const ARRAY_ALLOC_SIZE_MSG: &str = + "Fatal error: requested array size exceeds the maximum allowed array size\n"; +/// Fatal error message when `range()` cannot represent the requested interval, +/// because `end - start + 1` overflows a signed 64-bit element count. Matches +/// PHP's `ValueError: The supplied range exceeds the maximum array size`. +pub(crate) const RANGE_SIZE_MSG: &str = + "Fatal error: The supplied range exceeds the maximum array size\n"; +/// Fatal error message when `buffer_new()` receives a negative length or a +/// length whose `len * stride` payload size does not fit in the machine word. +/// `buffer_new` is an elephc extension with no PHP equivalent, so the wording is +/// elephc's own rather than a PHP parity string. +pub(crate) const BUFFER_ALLOC_SIZE_MSG: &str = + "Fatal error: buffer_new() length is negative or exceeds the maximum buffer size\n"; +/// Fatal error message when a runtime string producer is asked for a result whose byte +/// count cannot be allocated: either the size computation itself wrapped (`str_repeat()`'s +/// `len * times`, an encoder's `2 * len` / `3 * len` expansion) or the requested size +/// exceeds the configured heap capacity. PHP reports the same class of failure as +/// `Fatal error: Possible integer overflow in memory allocation (...)`; elephc's runtime +/// has no per-call-site operand context, so it reports the shared cause. +pub(crate) const ALLOC_OVERFLOW_MSG: &str = + "Fatal error: Possible integer overflow in memory allocation\n"; /// Fatal error message when `str_repeat()` receives a `$times` argument less than 0. pub(crate) const STR_REPEAT_TIMES_MSG: &str = "Fatal error: str_repeat(): Argument #2 ($times) must be greater than or equal to 0\n"; +/// Fatal error message when a `printf`-family conversion requests a field width outside +/// PHP's accepted range. PHP raises `ValueError: Width must be between 0 and 2147483647`; +/// elephc has no catchable-error path inside `__rt_sprintf`, so it reports the same text +/// as a controlled fatal instead of writing past the conversion buffer. +pub(crate) const SPRINTF_WIDTH_MSG: &str = + "Fatal error: Uncaught ValueError: Width must be between 0 and 2147483647\n"; +/// Fatal error message when a `printf`-family conversion would write past the shared +/// 64 KiB `_concat_buf` result arena. PHP grows its result buffer on the heap; elephc's +/// formatted results live in the fixed concat arena, so an oversized result is reported +/// instead of overrunning the arena. +pub(crate) const SPRINTF_OVERFLOW_MSG: &str = + "Fatal error: sprintf(): formatted result exceeds the 65536-byte string buffer\n"; +/// Fatal error message when a `printf`-family format string consumes more arguments than +/// were supplied. PHP raises `ArgumentCountError`; elephc reports the same class of error +/// as a controlled fatal because the alternative is reading past the pushed argument records. +pub(crate) const SPRINTF_ARGCOUNT_MSG: &str = + "Fatal error: Uncaught ArgumentCountError: sprintf(): too few arguments\n"; +/// Fatal error message when a `printf`-family format string uses a conversion character +/// PHP does not define. The runtime never forwards an unrecognized conversion to libc +/// `snprintf` (that would expose `%n` and friends), so it reports PHP's `ValueError` instead. +pub(crate) const SPRINTF_UNKNOWN_SPEC_MSG: &str = + "Fatal error: Uncaught ValueError: Unknown format specifier\n"; /// Catchable `\ValueError` message when `hash()` receives an unknown algorithm name. pub(crate) const HASH_UNKNOWN_ALGO_MSG: &str = "hash(): Argument #1 ($algo) must be a valid hashing algorithm"; diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 94f7f55a9e..3b4ebe056e 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -10,6 +10,8 @@ use std::collections::{HashMap, HashSet}; +use crate::codegen_support::data_section::comm_directive; +use crate::codegen_support::platform::Target; use crate::names::{ enum_case_symbol, function_variant_active_symbol, interface_method_wrapper_symbol, mangle_fqn, method_symbol, php_symbol_key, static_method_symbol, static_property_symbol, @@ -63,27 +65,28 @@ pub(crate) fn emit_runtime_data_user( allowed_class_names: Option<&HashSet>, emit_eval_reflection_metadata: bool, source_path: Option<&str>, + target: Target, ) -> String { let mut out = String::new(); let mut sorted_globals: Vec<&String> = global_var_names.iter().collect(); sorted_globals.sort(); for name in sorted_globals { - out.push_str(&format!(".comm _gvar_{}, 16, 3\n", name)); + out.push_str(&comm_directive(&format!("_gvar_{}", name), 16, target)); } let mut sorted_statics: Vec<&(String, String)> = static_vars.keys().collect(); sorted_statics.sort(); for (func_name, var_name) in sorted_statics { - out.push_str(&format!( - ".comm _static_{}_{}, 16, 3\n", - mangle_fqn(func_name), - var_name + out.push_str(&comm_directive( + &format!("_static_{}_{}", mangle_fqn(func_name), var_name), + 16, + target, )); - out.push_str(&format!( - ".comm _static_{}_{}_init, 8, 3\n", - mangle_fqn(func_name), - var_name + out.push_str(&comm_directive( + &format!("_static_{}_{}_init", mangle_fqn(func_name), var_name), + 8, + target, )); } @@ -104,7 +107,7 @@ pub(crate) fn emit_runtime_data_user( let mut static_property_symbols: Vec = static_property_symbols.into_iter().collect(); static_property_symbols.sort(); for symbol in static_property_symbols { - out.push_str(&format!(".comm {}, 16, 3\n", symbol)); + out.push_str(&comm_directive(&symbol, 16, target)); } let mut sorted_enum_names: Vec<&String> = enums.keys().collect(); @@ -114,9 +117,10 @@ pub(crate) fn emit_runtime_data_user( continue; }; for case in &enum_info.cases { - out.push_str(&format!( - ".comm {}, 8, 3\n", - enum_case_symbol(*enum_name, &case.name) + out.push_str(&comm_directive( + &enum_case_symbol(*enum_name, &case.name), + 8, + target, )); } } @@ -277,6 +281,60 @@ pub(crate) fn emit_runtime_data_user( } } + // Per-class print_r / var_export descriptor pointer table — read by + // `__rt_print_r_object` and by the `__elephc_object_prop_*` prelude helpers. + // Same rows as `_class_vd_desc_ptrs`, different key spellings. + out.push_str(".globl _class_prop_desc_ptrs\n_class_prop_desc_ptrs:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + if class_info_by_id.contains_key(&class_id) { + out.push_str(&format!(" .quad _class_prop_desc_{}\n", class_id)); + } else { + out.push_str(" .quad _class_prop_desc_missing\n"); + } + } + } + + // Per-class enum tables. `_class_enum_kinds` is 0 for an ordinary class and + // 1/2/3 for a pure / int-backed / string-backed enum — `print_r` prints + // `E Enum`, `E Enum:int` and `E Enum:string` respectively, and `var_dump` / + // `var_export` only need the non-zero test. `_class_enum_name_offsets` is the + // byte offset of the enum's `name` property slot inside the instance (`-1` + // for a non-enum), which is where the case name every renderer prints lives. + // Both are indexed by the same class id as every other per-class table, so an + // enum instance is recognized from its object header alone. + out.push_str(".globl _class_enum_kinds\n_class_enum_kinds:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let kind = class_name_by_id + .get(&class_id) + .and_then(|class_name| enums.get(*class_name)) + .map(|enum_info| match &enum_info.backing_type { + Some(PhpType::Int) => 2u64, + Some(PhpType::Str) => 3u64, + _ => 1u64, + }) + .unwrap_or(0); + out.push_str(&format!(" .quad {}\n", kind)); + } + } + + out.push_str(".globl _class_enum_name_offsets\n_class_enum_name_offsets:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let offset = match ( + class_name_by_id.get(&class_id), + class_info_by_id.get(&class_id), + ) { + (Some(class_name), Some(class_info)) if enums.contains_key(*class_name) => { + enum_case_name_property_offset(class_info) + } + _ => -1, + }; + out.push_str(&format!(" .quad {}\n", offset)); + } + } + // JsonException's class_id is consulted by __rt_json_throw_error when // JSON_THROW_ON_ERROR is set — it allocates an instance of this class // and routes it through the normal exception machinery. @@ -509,6 +567,12 @@ pub(crate) fn emit_runtime_data_user( out.push_str(" .p2align 3\n"); out.push_str(".globl _class_vd_desc_missing\n_class_vd_desc_missing:\n"); out.push_str(" .quad 0\n"); // property count = 0 + // _class_prop_desc_missing: zero properties (a class id with no print_r / + // var_export metadata), so an unknown class renders an empty body instead of + // reading past the table. + out.push_str(" .p2align 3\n"); + out.push_str(".globl _class_prop_desc_missing\n_class_prop_desc_missing:\n"); + out.push_str(" .quad 0\n"); // property count = 0 out.push_str(" .p2align 3\n"); out.push_str(".globl _class_vtable_missing\n_class_vtable_missing:\n"); out.push_str(" .quad 0\n"); @@ -941,7 +1005,10 @@ pub(crate) fn emit_runtime_data_user( // (see `var_dump_debug_info_projection`), because `var_dump` is the only PHP // renderer that consults `__debugInfo` AND the only elephc renderer that // enumerates object properties at all. - let vd_rows = var_dump_descriptor_rows(class_info, class_name); + let mut vd_rows = var_dump_descriptor_rows(class_info, class_name); + if enums.contains_key(class_name.as_str()) { + hoist_enum_name_row(&mut vd_rows); + } for (row_index, row) in vd_rows.iter().enumerate() { out.push_str(&format!( ".globl _class_vd_pkey_{}_{}\n_class_vd_pkey_{}_{}:\n .ascii \"{}\"\n", @@ -975,6 +1042,46 @@ pub(crate) fn emit_runtime_data_user( out.push_str(&format!(" .quad {}\n", row.type_name.len())); // declared type-name byte length } + // print_r / var_export property table: the SAME rows (and therefore the + // same `__debugInfo()` projection, offsets and value tags) as the + // var_dump descriptor above, but carrying the two other key spellings PHP + // uses for the same property — `print_r`'s unquoted `x` / `y:protected` / + // `z:C:private`, and `var_export`'s bare `x`. Sharing one row list is what + // keeps the three renderers from ever disagreeing about which properties + // an object has or where they live. + for (row_index, row) in vd_rows.iter().enumerate() { + out.push_str(&format!( + ".globl _class_prop_pkey_{}_{}\n_class_prop_pkey_{}_{}:\n .ascii \"{}\"\n", + class_info.class_id, row_index, class_info.class_id, row_index, + escaped_ascii(&row.print_r_key), + )); + out.push_str(&format!( + ".globl _class_prop_nkey_{}_{}\n_class_prop_nkey_{}_{}:\n .ascii \"{}\"\n", + class_info.class_id, row_index, class_info.class_id, row_index, + escaped_ascii(&row.plain_key), + )); + } + out.push_str(" .p2align 3\n"); + out.push_str(&format!( + ".globl _class_prop_desc_{}\n_class_prop_desc_{}:\n", + class_info.class_id, class_info.class_id, + )); + out.push_str(&format!(" .quad {}\n", vd_rows.len())); + for (row_index, row) in vd_rows.iter().enumerate() { + out.push_str(&format!( + " .quad _class_prop_pkey_{}_{}\n", + class_info.class_id, row_index + )); + out.push_str(&format!(" .quad {}\n", row.print_r_key.len())); // print_r key byte length + out.push_str(&format!(" .quad {}\n", row.offset)); // byte offset within the object + out.push_str(&format!(" .quad {}\n", row.tag)); // runtime value tag + out.push_str(&format!( + " .quad _class_prop_nkey_{}_{}\n", + class_info.class_id, row_index + )); + out.push_str(&format!(" .quad {}\n", row.plain_key.len())); // bare property-name byte length + } + out.push_str(" .p2align 3\n"); out.push_str(&format!(".globl _class_vtable_{}\n_class_vtable_{}:\n", class_info.class_id, class_info.class_id)); if class_info.vtable_methods.is_empty() { @@ -2430,6 +2537,13 @@ fn mangled_property_name(class_info: &ClassInfo, class_name: &str, prop_name: &s struct VarDumpRow { /// Text PHP renders between the `[` and `]`, quotes included. key: String, + /// Text PHP's `print_r` renders between the `[` and `]`: the same visibility + /// annotation as `key` but WITHOUT the double quotes (`x`, `y:protected`, + /// `z:C:private`). Consumed by `__rt_print_r_object`. + print_r_key: String, + /// Bare property name, which `var_export` prints with no visibility suffix + /// at all. Consumed by `__elephc_object_prop_name`. + plain_key: String, /// Byte offset of the backing property within the object. offset: usize, /// Runtime value tag of the backing property. @@ -2452,6 +2566,11 @@ fn var_dump_descriptor_rows(class_info: &ClassInfo, class_name: &str) -> Vec Vec Hearts` then `[value] => H` +/// (`print_r`), and `var_export` follows the same order. elephc lays a backed +/// enum's storage out with `value` first, so the two disagree unless the DISPLAY +/// order is fixed here. Rows carry an explicit byte offset, so reordering them is +/// purely cosmetic — every row still points at the same slot. Applied only to +/// enum classes, and a no-op when `name` is already first or absent. +fn hoist_enum_name_row(rows: &mut Vec) { + if let Some(index) = rows.iter().position(|row| row.plain_key == "name") { + let row = rows.remove(index); + rows.insert(0, row); + } +} + +/// Returns the byte offset of an enum class's `name` property slot, or `-1` when +/// the class does not declare one. +/// +/// Every PHP enum case exposes a readonly `name` holding the case identifier, and +/// elephc materializes it as an ordinary declared string property — so the case +/// name `enum(E::C)`, `E Enum` bodies and `\E::C` all print is just that slot's +/// 16-byte `(ptr, len)` pair. A `-1` result makes the runtime treat the class as a +/// plain object rather than reading a slot that may not exist. +fn enum_case_name_property_offset(class_info: &ClassInfo) -> i64 { + class_info + .properties + .iter() + .enumerate() + .find(|(_, (prop_name, _))| prop_name == "name") + .map(|(layout_index, (prop_name, _))| { + class_info + .property_offsets + .get(prop_name) + .copied() + .unwrap_or(8 + layout_index * 16) as i64 + }) + .unwrap_or(-1) +} + +/// Renders the text PHP prints between the `[` and `]` of a declared property's +/// `print_r` key line. +/// +/// `print_r` annotates visibility like `var_dump` does but WITHOUT quoting either +/// the property name or the declaring class: `x`, `y:protected`, `z:C:private` +/// (verified against PHP 8.4). The declaring class comes from the same +/// `property_declaring_classes` map `var_dump_property_key` reads, so the two +/// renderings can never name a different class for one property. +fn print_r_property_key(class_info: &ClassInfo, class_name: &str, prop_name: &str) -> String { + match class_info.property_visibilities.get(prop_name) { + Some(Visibility::Protected) => format!("{}:protected", prop_name), + Some(Visibility::Private) => { + let declaring = class_info + .property_declaring_classes + .get(prop_name) + .map(String::as_str) + .unwrap_or(class_name); + format!("{}:{}:private", prop_name, declaring) + } + _ => prop_name.to_string(), + } +} + /// Renders the declared type name PHP prints inside `uninitialized(...)` for a /// typed property read before its first write. /// @@ -2633,6 +2816,8 @@ fn prop_value_tag(class_info: &ClassInfo, prop_name: &str, prop_ty: &PhpType) -> mod tests { use std::collections::{HashMap, HashSet}; + use crate::codegen_support::platform::{Arch, Platform, Target}; + use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -2746,6 +2931,10 @@ mod tests { Some(&allowed_class_names), false, None, + Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + }, ); assert!(asm.contains("_class_vtable_1")); @@ -2777,6 +2966,10 @@ mod tests { None, false, None, + Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + }, ); assert!(asm.contains("_class_gc_desc_count:\n .quad 4\n")); @@ -2815,6 +3008,10 @@ mod tests { None, false, None, + Target { + platform: Platform::MacOS, + arch: Arch::AArch64, + }, ); assert!(asm.contains("_class_gc_desc_1:\n .byte 10\n")); diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index 6acf011b36..117cb4654d 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -11,7 +11,10 @@ mod managed; mod platform; -use super::{callables, diagnostics, exceptions, generators, strings, system}; +use super::{ + callables, diagnostics, exceptions, generators, numeric, round_mode, strings, + system, +}; use crate::codegen_support::emit::Emitter; use crate::codegen_support::RuntimeFeatures; @@ -23,22 +26,33 @@ use crate::codegen_support::RuntimeFeatures; pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { diagnostics::emit_diagnostics(emitter); + // Shared numeric coercions. Emitted first because string, array, and cast helpers all + // branch into `__rt_php_float_to_int` for PHP's float→int rules. + numeric::emit_php_float_to_int(emitter); + round_mode::emit_round_mode(emitter); + // String runtime functions + strings::emit_concat_scratch(emitter); strings::emit_itoa(emitter); strings::emit_resource_to_string(emitter); strings::emit_resource_type_name(emitter); strings::emit_resource_write_stdout(emitter); + strings::emit_php_num_scan(emitter); strings::emit_ftoa(emitter); + strings::emit_ftoa_repr(emitter); strings::emit_concat(emitter); strings::emit_atoi(emitter); strings::emit_str_eq(emitter); strings::emit_str_to_number(emitter); strings::emit_str_looks_like_int_for_coercion(emitter); strings::emit_str_to_int(emitter); + strings::emit_str_to_int_base(emitter); strings::emit_str_loose_eq(emitter); strings::emit_number_format(emitter); strings::emit_strcopy(emitter); strings::emit_str_persist(emitter); + strings::emit_str_inc_dec(emitter); + strings::emit_mixed_inc_dec(emitter); strings::emit_strtolower(emitter); strings::emit_strtoupper(emitter); strings::emit_trim(emitter); @@ -46,12 +60,16 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_rtrim(emitter); strings::emit_strpos(emitter); strings::emit_strrpos(emitter); + strings::emit_stripos(emitter); + strings::emit_strripos(emitter); strings::emit_str_repeat(emitter); strings::emit_strrev(emitter); strings::emit_grapheme_strrev(emitter); strings::emit_chr(emitter); strings::emit_strcmp(emitter); strings::emit_strcasecmp(emitter); + strings::emit_strncmp(emitter); + strings::emit_strncasecmp(emitter); strings::emit_str_starts_with(emitter); strings::emit_str_ends_with(emitter); strings::emit_str_replace(emitter); @@ -62,13 +80,23 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { strings::emit_ucwords(emitter); strings::emit_str_ireplace(emitter); strings::emit_substr_replace(emitter); + strings::emit_substr_count(emitter); strings::emit_str_pad(emitter); strings::emit_str_split(emitter); strings::emit_addslashes(emitter); strings::emit_stripslashes(emitter); strings::emit_nl2br(emitter); + strings::emit_chunk_split(emitter); + strings::emit_quotemeta(emitter); + strings::emit_quoted_printable_encode(emitter); + strings::emit_str_word_count(emitter); + strings::emit_count_chars(emitter); + strings::emit_strtr(emitter); strings::emit_wordwrap(emitter); strings::emit_bin2hex(emitter); + strings::emit_dec_to_base(emitter); + strings::emit_base_to_number(emitter); + strings::emit_base_convert(emitter); strings::emit_long2ip(emitter); strings::emit_ip2long(emitter); strings::emit_inet_ntop(emitter); @@ -157,6 +185,8 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { system::emit_preg_split(emitter); } system::emit_match_unhandled(emitter); + system::emit_stack_limit_init(emitter); + system::emit_stack_overflow(emitter); // Exception runtime functions exceptions::emit_exception_cleanup_frames(emitter); diff --git a/src/codegen_support/runtime/emitters/managed.rs b/src/codegen_support/runtime/emitters/managed.rs index da1ccdc1b0..a1f114d3b8 100644 --- a/src/codegen_support/runtime/emitters/managed.rs +++ b/src/codegen_support/runtime/emitters/managed.rs @@ -7,7 +7,9 @@ //! Key details: //! - Keeps heap, GC, eval-scope, SPL, object, and buffer helpers in dependency order. -use super::super::{arrays, buffers, eval_bridge, eval_scope, objects, resource_ids, spl}; +use super::super::{ + arrays, buffers, compare, eval_bridge, eval_scope, objects, resource_ids, spl, +}; use crate::codegen_support::emit::Emitter; use crate::codegen_support::RuntimeFeatures; @@ -81,6 +83,8 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_array_merge_refcounted(emitter); arrays::emit_array_slice(emitter); arrays::emit_array_slice_refcounted(emitter); + arrays::emit_array_slice_to_hash(emitter); + arrays::emit_array_chunk_to_hash(emitter); arrays::emit_range(emitter); arrays::emit_shuffle(emitter); arrays::emit_array_unique(emitter); @@ -96,9 +100,13 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_array_diff_refcounted(emitter); arrays::emit_array_is_list(emitter); arrays::emit_array_edge_key(emitter); + arrays::emit_array_ptr_seek(emitter); + arrays::emit_array_ptr_key(emitter); + arrays::emit_array_ptr_value(emitter); arrays::emit_array_intersect(emitter); arrays::emit_array_intersect_refcounted(emitter); arrays::emit_array_flip(emitter); + arrays::emit_array_count_values(emitter); arrays::emit_array_flip_string(emitter); arrays::emit_hash_flip(emitter); arrays::emit_hash_map(emitter); @@ -114,9 +122,16 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_array_column_str(emitter); arrays::emit_array_splice(emitter); arrays::emit_array_splice_refcounted(emitter); + arrays::emit_array_splice_insert(emitter); + arrays::emit_array_splice_insert_refcounted(emitter); + arrays::emit_array_splice_insert_boxed(emitter); + arrays::emit_array_splice_insert_unboxed(emitter); + arrays::emit_array_splice_str(emitter); + arrays::emit_array_splice_insert_str(emitter); arrays::emit_array_diff_key(emitter); arrays::emit_array_intersect_key(emitter); arrays::emit_array_to_hash(emitter); + arrays::emit_array_to_hash_reverse(emitter); arrays::emit_array_replace(emitter); arrays::emit_array_replace_recursive(emitter); arrays::emit_assoc_diff_intersect(emitter); @@ -124,7 +139,7 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_array_merge_recursive(emitter); arrays::emit_array_multisort(emitter); arrays::emit_asort(emitter); - arrays::emit_ksort(emitter); + arrays::emit_hash_sort(emitter); arrays::emit_natsort(emitter); arrays::emit_array_map(emitter); arrays::emit_array_map_mixed(emitter); @@ -134,10 +149,12 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_array_filter_refcounted(emitter); arrays::emit_array_find_any_all(emitter); arrays::emit_array_reduce(emitter); + arrays::emit_array_reduce_str(emitter); arrays::emit_array_walk(emitter); arrays::emit_array_walk_recursive(emitter); arrays::emit_array_udiff_uintersect(emitter); arrays::emit_usort(emitter); + arrays::emit_usort_str(emitter); arrays::emit_array_to_mixed(emitter); arrays::emit_array_merge_into(emitter); arrays::emit_array_merge_into_refcounted(emitter); @@ -157,12 +174,15 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu arrays::emit_mixed_cast_bool(emitter); arrays::emit_mixed_cast_float(emitter); arrays::emit_mixed_cast_int(emitter); + arrays::emit_mixed_intval_base(emitter); arrays::emit_mixed_cast_string(emitter); arrays::emit_mixed_count(emitter); arrays::emit_mixed_free_deep(emitter); arrays::emit_mixed_is_empty(emitter); arrays::emit_mixed_numeric_binops(emitter); arrays::emit_int_checked_binops(emitter); + arrays::emit_int_pow_checked(emitter); + arrays::emit_mixed_numeric_pow(emitter); arrays::emit_mixed_strict_eq(emitter); arrays::emit_array_strict_eq(emitter); arrays::emit_mixed_unbox(emitter); @@ -202,6 +222,24 @@ pub(super) fn emit_managed_runtime(emitter: &mut Emitter, features: RuntimeFeatu objects::emit_mixed_array_fetch_for_write(emitter); objects::emit_new_by_name(emitter); objects::emit_call_object_destructor(emitter); + // PHP `==` walkers: one boxed-Mixed dispatcher plus the array and object + // recursion it delegates to. Emitted after the object property accessors it calls. + compare::emit_mixed_loose_eq(emitter); + compare::emit_mixed_array_loose_eq(emitter); + compare::emit_obj_loose_eq(emitter); + compare::emit_php_compare(emitter); + arrays::emit_min_max_mixed(emitter); + arrays::emit_min_max_str(emitter); + arrays::emit_min_max_hash(emitter); + objects::emit_obj_enum_kind(emitter); + objects::emit_obj_enum_name_offset(emitter); + objects::emit_obj_enum_case_name(emitter); + objects::emit_var_dump_emit_enum_line(emitter); + objects::emit_pr_obj_desc(emitter); + objects::emit_print_r_object(emitter); + objects::emit_obj_prop_count(emitter); + objects::emit_obj_prop_name(emitter); + objects::emit_obj_prop_value(emitter); objects::emit_json_encode_stdclass(emitter); // Buffer runtime functions diff --git a/src/codegen_support/runtime/emitters/platform.rs b/src/codegen_support/runtime/emitters/platform.rs index 6afc0b4a92..761ab0f6ff 100644 --- a/src/codegen_support/runtime/emitters/platform.rs +++ b/src/codegen_support/runtime/emitters/platform.rs @@ -185,6 +185,7 @@ pub(super) fn emit_platform_runtime(emitter: &mut Emitter, features: RuntimeFeat io::emit_ob_get_status(emitter); io::emit_ob_list_handlers(emitter); io::emit_file_get_contents(emitter); + io::emit_file_get_contents_range(emitter); io::emit_file_put_contents(emitter); io::emit_file(emitter); io::emit_stat(emitter); diff --git a/src/codegen_support/runtime/eval_bridge/aarch64_numeric.rs b/src/codegen_support/runtime/eval_bridge/aarch64_numeric.rs index ba347c0601..4242997b02 100644 --- a/src/codegen_support/runtime/eval_bridge/aarch64_numeric.rs +++ b/src/codegen_support/runtime/eval_bridge/aarch64_numeric.rs @@ -106,9 +106,7 @@ pub(super) fn emit_aarch64_numeric(emitter: &mut Emitter) { emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 - emitter.instruction("fdiv d2, d0, d1"); // compute the fmod quotient before truncation - emitter.instruction("frintz d2, d2"); // truncate the quotient toward zero - emitter.instruction("fmsub d0, d2, d1, d0"); // compute dividend minus truncated quotient times divisor + emitter.bl_c("fmod"); // libc fmod keeps the dividend's sign, so -0.0 survives like PHP emitter.instruction("fcmp d0, d0"); // detect NaN so PHP echo prints NAN without a sign emitter.instruction("b.vs __elephc_eval_value_fmod_nan"); // normalize unordered fmod results before boxing emitter.instruction("fmov x1, d0"); // move the fmod result bits into mixed value_lo diff --git a/src/codegen_support/runtime/fibers/alloc.rs b/src/codegen_support/runtime/fibers/alloc.rs index 0ce4482591..c7a1bb2300 100644 --- a/src/codegen_support/runtime/fibers/alloc.rs +++ b/src/codegen_support/runtime/fibers/alloc.rs @@ -15,7 +15,7 @@ use crate::codegen_support::platform::{Arch, Platform}; /// stack. Set to 16 KB so the guard fully covers a single page on every /// supported target — macOS aarch64 uses 16 KB pages, Linux aarch64 typically /// uses 4 KB but accepts oversized protection ranges silently. -const FIBER_GUARD_PAGE_SIZE: i32 = 16384; +pub(super) const FIBER_GUARD_PAGE_SIZE: i32 = 16384; /// Returns the platform-specific flag word for `MAP_PRIVATE | MAP_ANONYMOUS`. /// - macOS: `0x1002` (MAP_PRIVATE | MAP_ANON) diff --git a/src/codegen_support/runtime/fibers/switch.rs b/src/codegen_support/runtime/fibers/switch.rs index 3c10a98af1..fddc34db76 100644 --- a/src/codegen_support/runtime/fibers/switch.rs +++ b/src/codegen_support/runtime/fibers/switch.rs @@ -22,8 +22,23 @@ use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::system::{ + STACK_GUARD_RESERVE_BYTES, STACK_LIMIT_MAIN_SYMBOL, STACK_LIMIT_SYMBOL, +}; -use super::{FIBER_OWN_CALL_FRAME_OFFSET, FIBER_OWN_EXC_HEAD_OFFSET, FIBER_SAVED_SP_OFFSET}; +use super::alloc::FIBER_GUARD_PAGE_SIZE; +use super::{ + FIBER_OWN_CALL_FRAME_OFFSET, FIBER_OWN_EXC_HEAD_OFFSET, FIBER_SAVED_SP_OFFSET, + FIBER_STACK_BASE_OFFSET, +}; + +/// Call-stack floor for a coroutine stack, measured from the fiber's mmap base. +/// +/// `stack_base` is the low address of the whole mapping, whose first `FIBER_GUARD_PAGE_SIZE` +/// bytes are the `PROT_NONE` guard. Adding the guard plus the shared reserve gives the +/// lowest address a compiled prologue may legally reach on that coroutine stack, leaving the +/// guard page itself as a hard backstop for anything the software check cannot see. +const FIBER_STACK_FLOOR_OFFSET: i64 = FIBER_GUARD_PAGE_SIZE as i64 + STACK_GUARD_RESERVE_BYTES; /// Total bytes saved on the stack by an AArch64 context switch (must stay 16-aligned). const AARCH64_SWITCH_SAVE_BYTES: i32 = 160; @@ -117,6 +132,7 @@ pub fn emit_fiber_switch(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "x12", "_exc_handler_top", 0); // restore the target fiber's handler chain head globally emitter.instruction(&format!("ldr x13, [x0, #{}]", FIBER_OWN_CALL_FRAME_OFFSET)); // x13 = target fiber's saved activation-record cleanup chain head abi::emit_store_reg_to_symbol(emitter, "x13", "_exc_call_frame_top", 0); // restore the target fiber's call-frame chain head globally + emit_adopt_fiber_stack_limit_aarch64(emitter); emitter.instruction(&format!("ldr x11, [x0, #{}]", FIBER_SAVED_SP_OFFSET)); // x11 = target fiber's saved SP emitter.instruction("mov sp, x11"); // adopt the target fiber's stack emitter.instruction("b __rt_fiber_switch_restore"); // proceed to restore callee-saved registers @@ -127,6 +143,8 @@ pub fn emit_fiber_switch(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "x12", "_exc_handler_top", 0); // restore the main thread handler chain head globally abi::emit_load_symbol_to_reg(emitter, "x13", "_fiber_main_saved_call_frame", 0); // x13 = main thread's saved activation-record cleanup chain head abi::emit_store_reg_to_symbol(emitter, "x13", "_exc_call_frame_top", 0); // restore the main thread call-frame chain head globally + abi::emit_load_symbol_to_reg(emitter, "x14", STACK_LIMIT_MAIN_SYMBOL, 0); // x14 = the OS-thread call-stack floor measured at process start + abi::emit_store_reg_to_symbol(emitter, "x14", STACK_LIMIT_SYMBOL, 0); // restore the main-thread floor now that the main stack is current again abi::emit_load_symbol_to_reg(emitter, "x11", "_fiber_main_saved_sp", 0); // x11 = main thread's saved SP emitter.instruction("mov sp, x11"); // adopt the main thread's stack @@ -146,6 +164,44 @@ pub fn emit_fiber_switch(emitter: &mut Emitter) { emitter.instruction("ret"); // resume the target context where it last yielded } +/// Publishes the incoming fiber's call-stack floor into `_stack_limit` (AArch64). +/// +/// Called with `x0` still holding the target `Fiber*`, before its saved SP is adopted. +/// A fiber runs on an mmap'd coroutine stack that has nothing to do with the OS thread +/// stack, so the guard would otherwise compare against a completely unrelated address; this +/// swap is what keeps the guard from misfiring the moment a generator or Fiber body starts. +/// A zero `stack_base` (a fiber whose stack allocation failed) publishes zero, which leaves +/// the guard inert instead of publishing a nonsensical low-address floor. +/// +/// Clobbers x9 (symbol scratch), x14, and x15, none of which carry switch state. +fn emit_adopt_fiber_stack_limit_aarch64(emitter: &mut Emitter) { + emitter.comment("adopt the target fiber's call-stack floor"); + emitter.instruction(&format!("ldr x14, [x0, #{}]", FIBER_STACK_BASE_OFFSET)); // x14 = low address of the target fiber's stack mapping + emitter.instruction("cbz x14, __rt_fiber_switch_limit_ready"); // an unallocated stack publishes zero and disables the guard + abi::emit_load_int_immediate(emitter, "x15", FIBER_STACK_FLOOR_OFFSET); + emitter.instruction("add x14, x14, x15"); // skip the guard page and the shared reserve to get the usable floor + emitter.label("__rt_fiber_switch_limit_ready"); + abi::emit_store_reg_to_symbol(emitter, "x14", STACK_LIMIT_SYMBOL, 0); // publish the coroutine floor for every prologue that runs on this stack +} + +/// Publishes the incoming fiber's call-stack floor into `_stack_limit` (x86_64 SysV). +/// +/// Mirrors `emit_adopt_fiber_stack_limit_aarch64`, reading the target `Fiber*` from `rdi` +/// before its saved SP is adopted. Clobbers rax and (in PIC mode) the borrowed GOT scratch +/// register the symbol helper protects itself. +fn emit_adopt_fiber_stack_limit_x86_64(emitter: &mut Emitter) { + emitter.comment("adopt the target fiber's call-stack floor"); + emitter.instruction(&format!( + "mov rax, QWORD PTR [rdi + {}]", + FIBER_STACK_BASE_OFFSET + )); // rax = low address of the target fiber's stack mapping + emitter.instruction("test rax, rax"); // did this fiber ever get a stack mapping? + emitter.instruction("jz __rt_fiber_switch_limit_ready"); // an unallocated stack publishes zero and disables the guard + emitter.instruction(&format!("add rax, {}", FIBER_STACK_FLOOR_OFFSET)); // skip the guard page and the shared reserve to get the usable floor + emitter.label("__rt_fiber_switch_limit_ready"); + abi::emit_store_reg_to_symbol(emitter, "rax", STACK_LIMIT_SYMBOL, 0); // publish the coroutine floor for every prologue that runs on this stack +} + /// Returns the total bytes reserved on a freshly-created fiber stack for the entry frame. /// /// ARM64: equal to `AARCH64_SWITCH_SAVE_BYTES` (160 bytes, 16-aligned). @@ -235,6 +291,7 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_handler_top", 0); // restore the target fiber's handler chain head globally emitter.instruction(&format!("mov r11, QWORD PTR [rdi + {}]", FIBER_OWN_CALL_FRAME_OFFSET)); // r11 = target fiber's cleanup chain head abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_call_frame_top", 0); // restore the target fiber's cleanup chain head globally + emit_adopt_fiber_stack_limit_x86_64(emitter); emitter.instruction(&format!("mov rsp, QWORD PTR [rdi + {}]", FIBER_SAVED_SP_OFFSET)); // adopt the target fiber's saved stack pointer emitter.instruction("jmp __rt_fiber_switch_restore"); // proceed to restore callee-saved registers @@ -244,6 +301,8 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_handler_top", 0); // restore the main thread handler chain head globally abi::emit_load_symbol_to_reg(emitter, "r11", "_fiber_main_saved_call_frame", 0); // r11 = main thread's saved cleanup chain head abi::emit_store_reg_to_symbol(emitter, "r11", "_exc_call_frame_top", 0); // restore the main thread cleanup chain head globally + abi::emit_load_symbol_to_reg(emitter, "rax", STACK_LIMIT_MAIN_SYMBOL, 0); // rax = the OS-thread call-stack floor measured at process start + abi::emit_store_reg_to_symbol(emitter, "rax", STACK_LIMIT_SYMBOL, 0); // restore the main-thread floor now that the main stack is current again abi::emit_load_symbol_to_reg(emitter, "rsp", "_fiber_main_saved_sp", 0); // adopt the main thread's saved stack pointer // -- restore callee-saved state from the target stack and return into it -- diff --git a/src/codegen_support/runtime/generators/coro.rs b/src/codegen_support/runtime/generators/coro.rs index 7d6255debf..ddb22fc6f9 100644 --- a/src/codegen_support/runtime/generators/coro.rs +++ b/src/codegen_support/runtime/generators/coro.rs @@ -17,6 +17,12 @@ //! suspend boundary re-raises a scheduled `pending_throw` *inside* the //! coroutine's own stack, `Generator::throw()` lands in an in-generator //! `try/catch` — the core of issue #329. +//! - `__rt_gen_suspend` owns PHP's auto-key bookkeeping: an explicit *integer* +//! key greater than every integer key yielded so far pushes the counter to +//! `key + 1`, so the next bare `yield` continues the numbering (PHP's +//! `largest_used_integer_key`). `__rt_gen_suspend_delegated` is the same +//! primitive minus that bookkeeping; `yield from` enters there because PHP +//! forwards delegated keys verbatim without renumbering. //! - Generator fields live at offsets 184..224, inside the Fiber `reserved` //! region that `__rt_fiber_construct` already zero-initialises. @@ -46,15 +52,55 @@ pub(crate) const GEN_AUTO_KEY_OFFSET: i32 = 208; /// Byte offset of `gen_delegated_iter`: inner iterator for `yield from`. pub(crate) const GEN_DELEGATED_ITER_OFFSET: i32 = 216; +/// Emits the ARM64 auto-key bookkeeping that fronts `__rt_gen_suspend`. +/// +/// PHP tracks a generator's implicit numbering with `largest_used_integer_key`: +/// an explicit *integer* key greater than every integer key yielded so far +/// becomes the new largest, so the next bare `yield` continues at `key + 1`. +/// Keys that are not integers (string, float, bool, null) and integer keys at or +/// below the largest one leave the counter untouched. `gen_auto_key` stores +/// `largest_used_integer_key + 1`, so the comparison is against `counter - 1` +/// and all arithmetic wraps exactly like PHP's (`PHP_INT_MAX` then a bare +/// `yield` produces `PHP_INT_MIN`). +/// +/// Runs before any prologue, so it may only use caller-saved scratch and must +/// preserve `x0`/`x1`. Conditional branches target a file-local label because +/// Mach-O rejects `cbz`/`b.cond` to a `.globl` symbol; the shared body is then +/// reached with an unconditional tail branch to `__rt_gen_suspend_delegated` +/// (a separate `.text` section on Linux, so fall-through is not an option). +fn emit_gen_auto_key_bookkeeping_arm64(emitter: &mut Emitter) { + emitter.instruction("cbz x0, __rt_gen_suspend_key_noted"); // a bare `yield` is numbered by the auto-key path instead + emitter.instruction("ldr x10, [x0]"); // x10 = runtime tag of the explicit key cell + emitter.instruction(&format!("cmp x10, #{}", INT_TAG)); // only integer keys participate in PHP's auto-numbering + emitter.instruction("b.ne __rt_gen_suspend_key_noted"); // string, float, bool, and null keys leave the counter alone + emitter.instruction("ldr x10, [x0, #8]"); // x10 = the explicit integer key payload + crate::codegen_support::abi::emit_load_symbol_to_reg(emitter, "x11", "_fiber_current", 0); // x11 = the generator coroutine currently running + emitter.instruction(&format!("ldr x12, [x11, #{}]", GEN_AUTO_KEY_OFFSET)); // x12 = next auto key = largest used integer key + 1 + emitter.instruction("sub x12, x12, #1"); // x12 = largest integer key used so far (wraps like PHP) + emitter.instruction("cmp x10, x12"); // does this key exceed every integer key yielded so far? + emitter.instruction("b.le __rt_gen_suspend_key_noted"); // keys at or below the largest never rewind the counter + emitter.instruction("add x10, x10, #1"); // PHP resumes implicit numbering one past the explicit key + emitter.instruction(&format!("str x10, [x11, #{}]", GEN_AUTO_KEY_OFFSET)); // persist the advanced counter for the next bare yield + emitter.label("__rt_gen_suspend_key_noted"); + emitter.instruction("b __rt_gen_suspend_delegated"); // continue in the shared suspend body with the key untouched +} + /// Emits `__rt_gen_suspend`, the `yield` suspension primitive shared by every -/// generated generator body. +/// generated generator body, plus its `__rt_gen_suspend_delegated` entry point. +/// +/// `__rt_gen_suspend` first applies PHP's auto-key bookkeeping (see +/// `emit_gen_auto_key_bookkeeping_arm64`) and then tail-branches into +/// `__rt_gen_suspend_delegated`, which records the yielded key/value into the +/// current generator's persistent slots (refcount-replacing the previous +/// occupants) and suspends via `__rt_fiber_suspend`. On resume it returns the +/// value delivered by the next `send()`/`next()` (owned by the caller), or — +/// when `Generator::throw()` scheduled a `pending_throw` — the fiber suspend +/// boundary re-raises that exception inside this generator's stack so a local +/// `try/catch` can handle it. /// -/// Records the yielded key/value into the current generator's persistent slots -/// (refcount-replacing the previous occupants), then suspends via -/// `__rt_fiber_suspend`. On resume it returns the value delivered by the next -/// `send()`/`next()` (owned by the caller), or — when `Generator::throw()` -/// scheduled a `pending_throw` — the fiber suspend boundary re-raises that -/// exception inside this generator's stack so a local `try/catch` can handle it. +/// `yield from` calls `__rt_gen_suspend_delegated` directly so a forwarded key +/// never advances the outer generator's counter, matching PHP (which does not +/// renumber delegated keys and can therefore produce duplicates). /// /// Input: `x0`/`rdi` = boxed key cell (NULL → auto-increment integer key); /// `x1`/`rsi` = boxed value cell (ownership moves into the generator). @@ -68,6 +114,11 @@ pub(crate) fn emit_gen_suspend(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: __rt_gen_suspend ---"); emitter.label_global("__rt_gen_suspend"); + emit_gen_auto_key_bookkeeping_arm64(emitter); + + emitter.blank(); + emitter.comment("--- runtime: __rt_gen_suspend_delegated ---"); + emitter.label_global("__rt_gen_suspend_delegated"); // -- prologue: park the boxed key/value and cache the generator object -- emitter.instruction("sub sp, sp, #48"); // reserve frame plus saved callee registers @@ -119,7 +170,32 @@ pub(crate) fn emit_gen_suspend(emitter: &mut Emitter) { emitter.instruction("ret"); // return the sent value to the generator body } -/// x86_64 implementation of `__rt_gen_suspend`. +/// x86_64 counterpart of `emit_gen_auto_key_bookkeeping_arm64` (see that +/// function for the PHP rule it implements). +/// +/// Runs before any prologue, so it may only use caller-saved scratch, must +/// preserve `rdi`/`rsi`, and must avoid `r11` (borrowed by the PIC symbol +/// loader). Reaches the shared body with an unconditional tail jump, mirroring +/// the ARM64 path. +fn emit_gen_auto_key_bookkeeping_x86_64(emitter: &mut Emitter) { + emitter.instruction("test rdi, rdi"); // was an explicit key supplied? + emitter.instruction("jz __rt_gen_suspend_key_noted"); // a bare `yield` is numbered by the auto-key path instead + emitter.instruction("mov r10, QWORD PTR [rdi]"); // r10 = runtime tag of the explicit key cell + emitter.instruction(&format!("cmp r10, {}", INT_TAG)); // only integer keys participate in PHP's auto-numbering + emitter.instruction("jne __rt_gen_suspend_key_noted"); // string, float, bool, and null keys leave the counter alone + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // r10 = the explicit integer key payload + crate::codegen_support::abi::emit_load_symbol_to_reg(emitter, "r8", "_fiber_current", 0); // r8 = the generator coroutine currently running + emitter.instruction(&format!("mov r9, QWORD PTR [r8 + {}]", GEN_AUTO_KEY_OFFSET)); // r9 = next auto key = largest used integer key + 1 + emitter.instruction("sub r9, 1"); // r9 = largest integer key used so far (wraps like PHP) + emitter.instruction("cmp r10, r9"); // does this key exceed every integer key yielded so far? + emitter.instruction("jle __rt_gen_suspend_key_noted"); // keys at or below the largest never rewind the counter + emitter.instruction("add r10, 1"); // PHP resumes implicit numbering one past the explicit key + emitter.instruction(&format!("mov QWORD PTR [r8 + {}], r10", GEN_AUTO_KEY_OFFSET)); // persist the advanced counter for the next bare yield + emitter.label("__rt_gen_suspend_key_noted"); + emitter.instruction("jmp __rt_gen_suspend_delegated"); // continue in the shared suspend body with the key untouched +} + +/// x86_64 implementation of `__rt_gen_suspend` and `__rt_gen_suspend_delegated`. /// /// Mirrors the ARM64 version using the System V ABI: generator object cached in /// `r12`, parked key/value in `r13`/`r14`, Mixed boxing via `rax`=tag, @@ -128,6 +204,11 @@ fn emit_gen_suspend_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: __rt_gen_suspend ---"); emitter.label_global("__rt_gen_suspend"); + emit_gen_auto_key_bookkeeping_x86_64(emitter); + + emitter.blank(); + emitter.comment("--- runtime: __rt_gen_suspend_delegated ---"); + emitter.label_global("__rt_gen_suspend_delegated"); // -- prologue: park the boxed key/value and cache the generator object -- emitter.instruction("push rbp"); // save caller frame pointer @@ -636,7 +717,7 @@ fn emit_gen_delegate_arm64(emitter: &mut Emitter) { emitter.instruction("bl __rt_incref"); // own a value reference for the outer suspend (it consumes one) emitter.instruction("mov x1, x0"); // value -> suspend argument 1 emitter.instruction("mov x0, x20"); // key -> suspend argument 0 - emitter.instruction("bl __rt_gen_suspend"); // suspend the outer generator; x0 = sent value (owned) on resume + emitter.instruction("bl __rt_gen_suspend_delegated"); // suspend the outer generator; delegated keys never renumber emitter.instruction("mov x20, x0"); // stash the sent value for forwarding // -- forward the sent value into the inner generator, advancing it -- emitter.instruction(&format!("ldr x9, [x19, #{}]", FIBER_STATE_OFFSET)); // reload the inner state before resuming @@ -691,7 +772,7 @@ fn emit_gen_delegate_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_incref"); // own a value reference for the outer suspend emitter.instruction("mov rsi, rax"); // value -> suspend argument 1 emitter.instruction("mov rdi, r13"); // key -> suspend argument 0 - emitter.instruction("call __rt_gen_suspend"); // suspend the outer generator; rax = sent value on resume + emitter.instruction("call __rt_gen_suspend_delegated"); // suspend the outer generator; delegated keys never renumber emitter.instruction("mov r13, rax"); // stash the sent value for forwarding emitter.instruction(&format!("mov r10, QWORD PTR [r12 + {}]", FIBER_STATE_OFFSET)); // reload the inner state before resuming emitter.instruction(&format!("cmp r10, {}", FIBER_STATE_SUSPENDED)); // can the inner generator accept the sent value? diff --git a/src/codegen_support/runtime/generators/mod.rs b/src/codegen_support/runtime/generators/mod.rs index b0b42453f6..fbceebcafa 100644 --- a/src/codegen_support/runtime/generators/mod.rs +++ b/src/codegen_support/runtime/generators/mod.rs @@ -23,7 +23,8 @@ use crate::codegen_support::emit::Emitter; /// Emits all `__rt_gen_*` runtime helpers for the current target. /// -/// Emits the fiber-backed `yield` suspension primitive (`__rt_gen_suspend`) +/// Emits the fiber-backed `yield` suspension primitive (`__rt_gen_suspend`, +/// which falls through into its `__rt_gen_suspend_delegated` entry point) /// followed by the `Generator` method accessors. Both are target-aware /// internally. pub(crate) fn emit_generator_runtime(emitter: &mut Emitter) { diff --git a/src/codegen_support/runtime/io/fgets.rs b/src/codegen_support/runtime/io/fgets.rs index e1777882e8..f62973f5ab 100644 --- a/src/codegen_support/runtime/io/fgets.rs +++ b/src/codegen_support/runtime/io/fgets.rs @@ -7,10 +7,18 @@ //! //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. +//! - `fgets()` has no caller-supplied length bound, so the line accumulates into a +//! `__rt_concat_reserve` window that doubles through `__rt_concat_grow` whenever it fills. +//! A line longer than the 64 KiB concat scratch therefore produces owned heap storage +//! instead of running past `_concat_buf` into the adjacent BSS globals. use crate::codegen_support::{emit::Emitter, platform::Arch}; use crate::codegen_support::abi; +/// Initial reserved line capacity, in bytes. Long enough that ordinary text lines never grow, +/// small enough that a `while (fgets($f))` loop still stays inside the shared concat scratch. +const INITIAL_LINE_CAPACITY: usize = 4096; + /// Reads one line from a file descriptor into the concat buffer. /// dispatches to `emit_fgets_linux_x86_64` on x86_64; falls through to ARM64 /// path otherwise. @@ -18,11 +26,12 @@ use crate::codegen_support::abi; /// ABI contract: /// - input: x0 = file descriptor (non-negative for valid fds, negative for /// errors such as failed fopen) -/// - output: x1 = pointer to line start in `_concat_buf`, x2 = line length -/// (includes trailing `\n` if one was present before EOF) +/// - output: x1 = pointer to line start (concat scratch or owned heap storage), +/// x2 = line length (includes trailing `\n` if one was present before EOF) /// - side effect: sets `_eof_flags[fd] = 1` when the stream is exhausted -/// - concat buffer offsets are advanced atomically; a line may be partial -/// if EOF or read error interrupts the stream +/// - the reservation is published with the final line length, so the shared concat +/// offset only moves for scratch-backed lines; a line may be partial if EOF or a +/// read error interrupts the stream pub fn emit_fgets(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_fgets_linux_x86_64(emitter); @@ -42,18 +51,21 @@ pub fn emit_fgets(emitter: &mut Emitter) { // -- set up stack frame -- emitter.label("__rt_fgets_fd_ok"); - emitter.instruction("sub sp, sp, #48"); // allocate 48 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #32"); // establish new frame pointer + emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish new frame pointer - // -- save fd and record starting position in concat_buf -- + // -- save fd and reserve an initial line window -- emitter.instruction("str x0, [sp, #0]"); // save file descriptor on stack - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current write offset - emitter.instruction("str x10, [sp, #8]"); // save start offset for calculating length later - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // compute write pointer: buf + offset - emitter.instruction("str x12, [sp, #16]"); // save start pointer for return value + emitter.instruction(&format!("mov x9, #{}", INITIAL_LINE_CAPACITY)); // start from the initial line capacity + emitter.instruction("str x9, [sp, #24]"); // save the current line capacity + emitter.instruction("mov x0, x9"); // request the initial capacity from the reservation front end + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the line + emitter.instruction("str x0, [sp, #16]"); // save start pointer for return value + emitter.instruction("mov x1, x0"); // publish the reservation start pointer + emitter.instruction("ldr x2, [sp, #24]"); // publish the full reserved capacity + emitter.instruction("bl __rt_concat_publish"); // claim the whole window so nested reads append after it + emitter.instruction("str xzr, [sp, #8]"); // the line starts empty // -- user-wrapper fd: read the line through stream_read instead of read() -- emitter.instruction("ldr x0, [sp, #0]"); // reload the file descriptor @@ -64,10 +76,27 @@ pub fn emit_fgets(emitter: &mut Emitter) { // -- read loop: one byte at a time until \n or EOF -- emitter.label("__rt_fgets_loop"); - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x1, x11, x10"); // buf pointer for read syscall + + // -- make room for one more byte before reading it -- + emitter.instruction("ldr x9, [sp, #8]"); // current line length + emitter.instruction("add x9, x9, #1"); // capacity needed once the next byte lands + emitter.instruction("ldr x10, [sp, #24]"); // current line capacity + emitter.instruction("cmp x9, x10"); // does the next byte still fit the reservation? + emitter.instruction("b.ls __rt_fgets_have_room"); // no growth needed for this iteration + emitter.instruction("lsl x10, x10, #1"); // double the line capacity + emitter.instruction("str x10, [sp, #24]"); // save the grown line capacity + emitter.instruction("ldr x1, [sp, #16]"); // old line buffer pointer + emitter.instruction("mov x2, #0"); // release the whole claimed window + emitter.instruction("bl __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("ldr x0, [sp, #16]"); // old line buffer pointer + emitter.instruction("ldr x1, [sp, #8]"); // bytes accumulated so far must survive the move + emitter.instruction("ldr x2, [sp, #24]"); // grown line capacity + emitter.instruction("bl __rt_concat_grow"); // move the accumulated line into a larger owned buffer + emitter.instruction("str x0, [sp, #16]"); // save the grown line buffer pointer + emitter.label("__rt_fgets_have_room"); + emitter.instruction("ldr x1, [sp, #16]"); // line buffer base pointer + emitter.instruction("ldr x10, [sp, #8]"); // current line length + emitter.instruction("add x1, x1, x10"); // buf pointer for read syscall // -- read 1 byte via syscall -- emitter.instruction("ldr x0, [sp, #0]"); // reload fd for read syscall @@ -84,16 +113,14 @@ pub fn emit_fgets(emitter: &mut Emitter) { emitter.instruction("cbz x0, __rt_fgets_eof"); // if 0 bytes read, we hit EOF } - // -- advance concat_off by 1 -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - emitter.instruction("add x10, x10, #1"); // advance by 1 byte - emitter.instruction("str x10, [x9]"); // store updated offset + // -- count the byte just appended to the line -- + emitter.instruction("ldr x11, [sp, #16]"); // line buffer base pointer + emitter.instruction("ldr x13, [sp, #8]"); // offset of byte just read + emitter.instruction("ldrb w14, [x11, x13]"); // load the byte we just read + emitter.instruction("add x13, x13, #1"); // advance the line length by 1 byte + emitter.instruction("str x13, [sp, #8]"); // store the updated line length // -- check if the byte we just read is \n -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("sub x13, x10, #1"); // offset of byte just read - emitter.instruction("ldrb w14, [x11, x13]"); // load the byte we just read emitter.instruction("cmp w14, #0x0A"); // compare with newline character emitter.instruction("b.eq __rt_fgets_done"); // if newline, line is complete emitter.instruction("b __rt_fgets_loop"); // otherwise continue reading @@ -106,6 +133,9 @@ pub fn emit_fgets(emitter: &mut Emitter) { // [sp,#8] tracks the accumulated line length; the line is returned // directly (this path does not reuse the _concat_buf-based done label). emitter.label("__rt_fgets_wrapper_entry"); + emitter.instruction("ldr x1, [sp, #16]"); // the wrapper path accumulates elsewhere, so give the reservation back + emitter.instruction("mov x2, #0"); // release the whole claimed window + emitter.instruction("bl __rt_concat_publish"); // hand the unused line window back to the shared scratch buffer emitter.instruction("str xzr, [sp, #8]"); // line length = 0 emitter.label("__rt_fgets_wrapper_loop"); emitter.instruction("ldr x0, [sp, #0]"); // reload the wrapper fd @@ -121,18 +151,20 @@ pub fn emit_fgets(emitter: &mut Emitter) { emitter.instruction("strb w13, [x12, x10]"); // append the byte to the line buffer emitter.instruction("add x10, x10, #1"); // advance the line length emitter.instruction("str x10, [sp, #8]"); // store the updated line length + emitter.instruction("strb w13, [sp, #32]"); // remember the byte across the chunk-release calls + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand this chunk's scratch window back before the next read + emitter.instruction("mov x0, x1"); // chunk ptr for release + emitter.instruction("bl __rt_decref_any"); // release the chunk before continuing or finishing the line + emitter.instruction("ldrb w13, [sp, #32]"); // reload the byte the wrapper produced emitter.instruction("cmp w13, #0x0A"); // is the byte a newline? - emitter.instruction("mov x0, x1"); // chunk ptr for release (flags preserved) - emitter.instruction("b.eq __rt_fgets_wrapper_last"); // newline: release this chunk, then finish the line - emitter.instruction("bl __rt_decref_any"); // not newline: release the chunk and keep reading + emitter.instruction("b.eq __rt_fgets_wrapper_done"); // newline: finish the line emitter.instruction("b __rt_fgets_wrapper_loop"); // read the next byte - emitter.label("__rt_fgets_wrapper_last"); - emitter.instruction("bl __rt_decref_any"); // release the newline chunk emitter.label("__rt_fgets_wrapper_done"); crate::codegen_support::abi::emit_symbol_address(emitter, "x1", "_user_wrapper_drain_buf"); // line pointer emitter.instruction("ldr x2, [sp, #8]"); // line length - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return the wrapper line (ptr/len) // -- nonblocking read miss: return accumulated bytes without EOF -- @@ -154,20 +186,18 @@ pub fn emit_fgets(emitter: &mut Emitter) { // -- return result string -- emitter.label("__rt_fgets_done"); emitter.instruction("ldr x1, [sp, #16]"); // return string start pointer - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset (end position) - emitter.instruction("ldr x11, [sp, #8]"); // reload start offset - emitter.instruction("sub x2, x10, x11"); // length = current offset - start offset + emitter.instruction("ldr x2, [sp, #8]"); // return the accumulated line length + emitter.instruction("bl __rt_concat_publish"); // shrink the claimed window down to the bytes actually read // -- restore frame and return -- - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return to caller } /// x86_64-specific fgets: mirrors the ARM64 path using x86_64 System V ABI. /// Input: rdi = file descriptor -/// Output: rax = line start pointer in _concat_buf, rdx = line length +/// Output: rax = line start pointer (concat scratch or owned heap storage), rdx = line length /// Side effect: sets _eof_flags[fd] = 1 when stream is exhausted fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); @@ -183,14 +213,16 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_fgets_fd_ok_x86"); emitter.instruction("push rbp"); // preserve the caller frame pointer while fgets() uses local spill slots emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved file descriptor and concat-buffer start metadata - emitter.instruction("sub rsp, 32"); // reserve aligned stack space for the stream read loop temporaries + emitter.instruction("sub rsp, 48"); // reserve aligned stack space for the stream read loop temporaries emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the file descriptor across the repeated libc read() calls in the fgets() loop - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // load the current concat-buffer absolute offset before appending the line bytes - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // preserve the starting concat-buffer offset so the final line length can be reconstructed - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // materialize the concat-buffer base address once for the x86_64 fgets() helper - emitter.instruction("lea r10, [r11 + r10]"); // compute the start pointer for the borrowed line slice that fgets() will return - emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // preserve the line start pointer for the final elephc string result + emitter.instruction(&format!("mov QWORD PTR [rbp - 40], {}", INITIAL_LINE_CAPACITY)); // start from the initial line capacity + emitter.instruction(&format!("mov rax, {}", INITIAL_LINE_CAPACITY)); // request the initial capacity from the reservation front end + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the line + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the line start pointer for the final elephc string result + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // publish the full reserved capacity + emitter.instruction("call __rt_concat_publish"); // claim the whole window so nested reads append after it + emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // the line starts empty // -- user-wrapper fd: read the line through stream_read instead of read() -- emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the file descriptor @@ -199,9 +231,26 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jge __rt_fgets_wrapper_entry_x86"); // wrappers read via the feof-gated stream_read loop below emitter.label("__rt_fgets_loop_x86"); - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the current concat-buffer absolute offset before reading one more byte - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // rematerialize the concat-buffer base address for the current one-byte read destination - emitter.instruction("lea rsi, [r11 + r10]"); // compute the address where libc read() should append the next byte + + // -- make room for one more byte before reading it -- + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // current line length + emitter.instruction("add r8, 1"); // capacity needed once the next byte lands + emitter.instruction("mov r9, QWORD PTR [rbp - 40]"); // current line capacity + emitter.instruction("cmp r8, r9"); // does the next byte still fit the reservation? + emitter.instruction("jbe __rt_fgets_have_room_x86"); // no growth needed for this iteration + emitter.instruction("add r9, r9"); // double the line capacity + emitter.instruction("mov QWORD PTR [rbp - 40], r9"); // save the grown line capacity + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // old line buffer pointer + emitter.instruction("xor edx, edx"); // release the whole claimed window + emitter.instruction("call __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // old line buffer pointer + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // bytes accumulated so far must survive the move + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // grown line capacity + emitter.instruction("call __rt_concat_grow"); // move the accumulated line into a larger owned buffer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the grown line buffer pointer + emitter.label("__rt_fgets_have_room_x86"); + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // line buffer base pointer + emitter.instruction("add rsi, QWORD PTR [rbp - 16]"); // compute the address where libc read() should append the next byte emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the tracked file descriptor as the first libc read() argument emitter.instruction("mov edx, 1"); // request exactly one byte so fgets() can stop on the first newline emitter.instruction("call read"); // read one byte from the stream through libc read() into the concat buffer @@ -211,22 +260,21 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_fgets_eof_x86"); // zero-byte read means real EOF emitter.label("__rt_fgets_read_ok_x86"); - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the previous concat-buffer absolute offset before publishing the appended byte - emitter.instruction("add r10, 1"); // advance the concat-buffer offset by the one byte that libc read() appended - abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // publish the updated concat-buffer offset for later string appenders - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // rematerialize the concat-buffer base address so the newly appended byte can be inspected - emitter.instruction("movzx ecx, BYTE PTR [r11 + r10 - 1]"); // load the byte that was just appended at the new concat-buffer tail + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // line buffer base pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // offset of the byte just read + emitter.instruction("movzx ecx, BYTE PTR [r11 + r10]"); // load the byte that was just appended to the line + emitter.instruction("add r10, 1"); // advance the line length by the one byte that libc read() appended + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the updated line length emitter.instruction("cmp cl, 0x0A"); // did the newly appended byte terminate the line with a newline? emitter.instruction("jne __rt_fgets_loop_x86"); // keep reading until fgets() hits newline, EOF, or read failure emitter.label("__rt_fgets_done_x86"); - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the concat-buffer start pointer for the borrowed fgets() line slice - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the concat-buffer absolute end offset after the line-read loop finishes - emitter.instruction("sub r10, QWORD PTR [rbp - 16]"); // compute the borrowed line length from the difference between end and start offsets - emitter.instruction("mov rdx, r10"); // return the borrowed line length in the x86_64 elephc string-length result register - emitter.instruction("add rsp, 32"); // release the fgets() spill slots before returning the borrowed line slice + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the reserved start pointer for the fgets() line + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // return the accumulated line length in the x86_64 elephc string-length result register + emitter.instruction("call __rt_concat_publish"); // shrink the claimed window down to the bytes actually read + emitter.instruction("add rsp, 48"); // release the fgets() spill slots before returning the line slice emitter.instruction("pop rbp"); // restore the caller frame pointer after the x86_64 fgets() helper completes - emitter.instruction("ret"); // return the borrowed concat-buffer line slice to the caller + emitter.instruction("ret"); // return the fgets() line slice to the caller // -- user-wrapper line read: feof-gated stream_read, one byte at a time. // feof is checked BEFORE each read so the loop never makes the EOF read @@ -236,6 +284,9 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { // [rbp-16] tracks the accumulated line length; the line is returned // directly (this path does not reuse the _concat_buf-based done label). emitter.label("__rt_fgets_wrapper_entry_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // the wrapper path accumulates elsewhere, so give the reservation back + emitter.instruction("xor edx, edx"); // release the whole claimed window + emitter.instruction("call __rt_concat_publish"); // hand the unused line window back to the shared scratch buffer emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // line length = 0 emitter.label("__rt_fgets_wrapper_loop_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the wrapper fd @@ -254,17 +305,19 @@ fn emit_fgets_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov BYTE PTR [r11 + r10], cl"); // append the byte to the line buffer emitter.instruction("add r10, 1"); // advance the line length emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the updated line length + emitter.instruction("mov BYTE PTR [rbp - 48], cl"); // remember the byte across the chunk-release calls + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand this chunk's scratch window back before the next read + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // chunk ptr for release + emitter.instruction("call __rt_decref_any"); // release the chunk before continuing or finishing the line + emitter.instruction("movzx ecx, BYTE PTR [rbp - 48]"); // reload the byte the wrapper produced emitter.instruction("cmp cl, 0x0A"); // is the byte a newline? - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // chunk ptr for release (flags preserved) - emitter.instruction("je __rt_fgets_wrapper_last_x86"); // newline: release this chunk, then finish the line - emitter.instruction("call __rt_decref_any"); // not newline: release the chunk and keep reading + emitter.instruction("je __rt_fgets_wrapper_done_x86"); // newline: finish the line emitter.instruction("jmp __rt_fgets_wrapper_loop_x86"); // read the next byte - emitter.label("__rt_fgets_wrapper_last_x86"); - emitter.instruction("call __rt_decref_any"); // release the newline chunk emitter.label("__rt_fgets_wrapper_done_x86"); abi::emit_symbol_address(emitter, "rax", "_user_wrapper_drain_buf"); // line pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // line length - emitter.instruction("add rsp, 32"); // release the fgets() spill slots + emitter.instruction("add rsp, 48"); // release the fgets() spill slots emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the wrapper line (ptr/len) diff --git a/src/codegen_support/runtime/io/file.rs b/src/codegen_support/runtime/io/file.rs index d16ebca3ef..84fc70180f 100644 --- a/src/codegen_support/runtime/io/file.rs +++ b/src/codegen_support/runtime/io/file.rs @@ -15,14 +15,25 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// Each line includes its trailing newline character (`\n`) except for the last line if the file /// does not end with a newline. Returns a pointer to a runtime array of strings. /// +/// PHP's `$flags` bitmask is applied while the lines are produced, so no line is ever allocated +/// and then trimmed or discarded: `FILE_IGNORE_NEW_LINES` (2) drops a trailing `\n` plus a +/// preceding `\r` before the line is pushed, and `FILE_SKIP_EMPTY_LINES` (4) then suppresses a +/// line that is left empty. php-src evaluates the two in exactly that order, which is why +/// `FILE_SKIP_EMPTY_LINES` alone keeps a bare `"\n"` line (its length is still 1). +/// `FILE_USE_INCLUDE_PATH` (1) is accepted and has no effect: elephc resolves includes at compile +/// time and has no run-time include path, which matches PHP's own default empty `include_path`. +/// /// Stack frame (ARM64, 64 bytes): /// - sp+#0..#7: file data pointer and length (saved across calls) /// - sp+#8..#15: scratch /// - sp+#16..#23: result array pointer (preserved across `__rt_array_push_str` calls) /// - sp+#24..#31: saved scan cursor when calling `__rt_array_push_str` -/// - sp+#32..#47: scratch +/// - sp+#32..#39: the `$flags` bitmask (saved across every call) +/// - sp+#40..#47: scratch /// - sp+#48..#63: saved x29/x30 /// +/// Input: x0 = `$flags`, x1 = filename pointer, x2 = filename length. +/// /// On x86_64 Linux, delegates to `emit_file_linux_x86_64` which follows the System V AMD64 ABI. pub fn emit_file(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -38,6 +49,7 @@ pub fn emit_file(emitter: &mut Emitter) { emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address emitter.instruction("add x29, sp, #48"); // establish new frame pointer + emitter.instruction("str x0, [sp, #32]"); // save the PHP $flags bitmask across every helper call // -- read entire file contents -- emitter.instruction("bl __rt_file_get_contents"); // read file, x1=ptr, x2=len @@ -66,14 +78,17 @@ pub fn emit_file(emitter: &mut Emitter) { emitter.instruction("cmp w6, #0x0A"); // compare with newline emitter.instruction("b.ne __rt_file_scan"); // if not newline, continue scanning - // -- found newline: push this line to array -- + // -- found newline: apply the PHP $flags bitmask, then push this line to array -- + emitter.instruction("sub x7, x3, x5"); // line start = current pos - line length + emit_file_line_flags_aarch64(emitter, "scan"); emitter.instruction("str x3, [sp, #24]"); // save scan pointer (push_str clobbers x3) emitter.instruction("ldr x0, [sp, #16]"); // reload array pointer - emitter.instruction("sub x1, x3, x5"); // line start = current pos - line length - emitter.instruction("mov x2, x5"); // line length (including \n) + emitter.instruction("mov x1, x7"); // line start pointer + emitter.instruction("mov x2, x5"); // line length after flag trimming emitter.instruction("bl __rt_array_push_str"); // push line to array (x0 = possibly new array) emitter.instruction("str x0, [sp, #16]"); // update array pointer after possible growth emitter.instruction("ldr x3, [sp, #24]"); // restore scan pointer + emitter.label("__rt_file_scan_skip"); emitter.instruction("mov x5, #0"); // reset line length for next line // -- reload scan state and continue -- @@ -84,10 +99,14 @@ pub fn emit_file(emitter: &mut Emitter) { // -- handle last line (no trailing newline) -- emitter.label("__rt_file_last"); emitter.instruction("cbz x5, __rt_file_ret"); // if last line is empty, skip it + emitter.instruction("sub x7, x3, x5"); // line start = current pos - line length + emit_file_line_flags_aarch64(emitter, "last"); emitter.instruction("ldr x0, [sp, #16]"); // reload array pointer - emitter.instruction("sub x1, x3, x5"); // line start = current pos - line length - emitter.instruction("mov x2, x5"); // line length + emitter.instruction("mov x1, x7"); // line start pointer + emitter.instruction("mov x2, x5"); // line length after flag trimming emitter.instruction("bl __rt_array_push_str"); // push last line to array + emitter.instruction("str x0, [sp, #16]"); // update array pointer after possible growth + emitter.label("__rt_file_last_skip"); // -- return array pointer -- emitter.label("__rt_file_ret"); @@ -99,6 +118,39 @@ pub fn emit_file(emitter: &mut Emitter) { emitter.instruction("ret"); // return to caller } +/// Emits the ARM64 `$flags` handling applied to one complete `file()` line before it is pushed. +/// +/// On entry `x7` is the line start pointer and `x5` its length including any terminator; on exit +/// `x5` is the length PHP would store. `FILE_IGNORE_NEW_LINES` (bit 1) removes a trailing `\n` and +/// then a trailing `\r`, so a CRLF file yields the same lines as an LF one. `FILE_SKIP_EMPTY_LINES` +/// (bit 2) is evaluated afterwards and branches to `__rt_file__skip`, suppressing the push +/// entirely; the ordering is what makes `FILE_SKIP_EMPTY_LINES` alone keep a bare `"\n"` line, the +/// same as php-src. +/// +/// `site` names the caller so the mid-loop and trailing-line copies get distinct local labels. +fn emit_file_line_flags_aarch64(emitter: &mut Emitter, site: &str) { + emitter.instruction("ldr x9, [sp, #32]"); // reload the PHP $flags bitmask + emitter.instruction("tst x9, #2"); // FILE_IGNORE_NEW_LINES requested? + emitter.instruction(&format!("b.eq __rt_file_{}_keep_eol", site)); // keep the terminator when the flag is clear + emitter.instruction(&format!("cbz x5, __rt_file_{}_keep_eol", site)); // an already-empty line has no terminator to drop + emitter.instruction("sub x10, x5, #1"); // index of the line's last byte + emitter.instruction("ldrb w11, [x7, x10]"); // load the line's last byte + emitter.instruction("cmp w11, #0x0A"); // is the line terminated by a line feed? + emitter.instruction(&format!("b.ne __rt_file_{}_keep_eol", site)); // nothing to trim without a line feed + emitter.instruction("mov x5, x10"); // drop the trailing line feed + emitter.instruction(&format!("cbz x5, __rt_file_{}_keep_eol", site)); // a lone line feed leaves an empty line + emitter.instruction("sub x10, x5, #1"); // index of the byte before the dropped line feed + emitter.instruction("ldrb w11, [x7, x10]"); // load that byte to detect a CRLF terminator + emitter.instruction("cmp w11, #0x0D"); // is the terminator a carriage return + line feed pair? + emitter.instruction(&format!("b.ne __rt_file_{}_keep_eol", site)); // a bare line feed needs no further trimming + emitter.instruction("mov x5, x10"); // drop the carriage return of a CRLF terminator + emitter.label(&format!("__rt_file_{}_keep_eol", site)); + emitter.instruction("tst x9, #4"); // FILE_SKIP_EMPTY_LINES requested? + emitter.instruction(&format!("b.eq __rt_file_{}_emit", site)); // keep every line when the flag is clear + emitter.instruction(&format!("cbz x5, __rt_file_{}_skip", site)); // suppress a line left empty by the trimming above + emitter.label(&format!("__rt_file_{}_emit", site)); +} + /// Emits the x86_64 Linux variant of `__rt_file` using the System V AMD64 ABI. /// /// Follows the same semantics as the ARM64 version: reads a file via `__rt_file_get_contents`, @@ -110,7 +162,10 @@ pub fn emit_file(emitter: &mut Emitter) { /// - rbp-16: owned file payload length (preserved across array operations) /// - rbp-24: result array pointer (updated after each `__rt_array_push_str` call) /// - rbp-32: scan cursor spill (preserved across `__rt_array_push_str`) +/// - rbp-40: the `$flags` bitmask (preserved across every call) /// Caller-saved registers r8–r11 and rcx hold the scan state. +/// +/// Input: rdi = `$flags`, plus the filename in the shared x86_64 elephc string registers. fn emit_file_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: file ---"); @@ -119,6 +174,7 @@ fn emit_file_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // preserve the caller frame pointer while file() uses scan state and array spill slots emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the file payload, scan cursors, and result array pointer emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the file payload, line scan cursors, and result array pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the PHP $flags bitmask across every helper call emitter.instruction("call __rt_file_get_contents"); // read the full file payload into an owned elephc string before splitting it into lines emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // preserve the owned file payload pointer across the later array allocation and line pushes @@ -144,16 +200,18 @@ fn emit_file_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp dl, 0x0A"); // test whether the consumed byte is a line-feed terminator emitter.instruction("jne __rt_file_scan"); // continue scanning the current line until a terminating line-feed is found + emit_file_line_flags_x86_64(emitter, "scan"); emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // preserve the active scan cursor because array_push_str() is free to clobber caller-saved registers emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the result array pointer into the x86_64 append-helper receiver register emitter.instruction("mov rsi, r9"); // pass the current line start pointer as the string payload argument to array_push_str() - emitter.instruction("mov rdx, rcx"); // pass the completed line length, including the trailing newline, to array_push_str() + emitter.instruction("mov rdx, rcx"); // pass the completed line length after flag trimming to array_push_str() emitter.instruction("call __rt_array_push_str"); // append the completed line slice as an owned string in the result array emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the updated array pointer after array_push_str() handles possible growth emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // restore the active scan cursor after the append helper clobbers caller-saved registers emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the full file payload length before rebuilding the end-of-buffer pointer emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the owned file payload base pointer before rebuilding the end-of-buffer pointer emitter.instruction("add r11, r10"); // rebuild the pointer one byte past the end of the owned file payload after the helper call + emitter.label("__rt_file_scan_skip"); emitter.instruction("mov r9, r8"); // start the next line at the scan cursor immediately after the consumed newline emitter.instruction("xor rcx, rcx"); // reset the current line length counter before scanning the next line emitter.instruction("jmp __rt_file_scan"); // continue scanning the remaining bytes in the file payload for more newline terminators @@ -161,11 +219,13 @@ fn emit_file_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_file_last"); emitter.instruction("test rcx, rcx"); // detect whether the file ended with a partial line that still needs to be appended emitter.instruction("jz __rt_file_cleanup"); // skip the final push when the file already ended exactly on a newline boundary + emit_file_line_flags_x86_64(emitter, "last"); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the result array pointer into the x86_64 append-helper receiver register emitter.instruction("mov rsi, r9"); // pass the trailing line start pointer as the string payload argument to array_push_str() - emitter.instruction("mov rdx, rcx"); // pass the trailing line length without a newline terminator to array_push_str() + emitter.instruction("mov rdx, rcx"); // pass the trailing line length after flag trimming to array_push_str() emitter.instruction("call __rt_array_push_str"); // append the trailing partial line as an owned string in the result array emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the updated array pointer after appending the trailing partial line + emitter.label("__rt_file_last_skip"); emitter.label("__rt_file_cleanup"); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the result array pointer in the canonical x86_64 integer result register @@ -173,3 +233,39 @@ fn emit_file_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the line array emitter.instruction("ret"); // return the array of file lines to the caller } + +/// Emits the x86_64 `$flags` handling applied to one complete `file()` line before it is pushed. +/// +/// Mirrors [`emit_file_line_flags_aarch64`] instruction for instruction: on entry `r9` is the line +/// start pointer and `rcx` its length including any terminator, and on exit `rcx` is the length +/// PHP would store. `FILE_IGNORE_NEW_LINES` (bit 1) drops a trailing `\n` and then a trailing `\r`; +/// `FILE_SKIP_EMPTY_LINES` (bit 2) is evaluated afterwards and jumps to `__rt_file__skip`. +/// +/// `site` names the caller so the mid-loop and trailing-line copies get distinct local labels. +fn emit_file_line_flags_x86_64(emitter: &mut Emitter, site: &str) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the PHP $flags bitmask + emitter.instruction("test rax, 2"); // FILE_IGNORE_NEW_LINES requested? + emitter.instruction(&format!("je __rt_file_{}_keep_eol", site)); // keep the terminator when the flag is clear + emitter.instruction("test rcx, rcx"); // does the line have any bytes to trim? + emitter.instruction(&format!("jz __rt_file_{}_keep_eol", site)); // an already-empty line has no terminator to drop + emitter.instruction("mov rsi, rcx"); // copy the line length before computing the last byte index + emitter.instruction("sub rsi, 1"); // index of the line's last byte + emitter.instruction("mov dl, BYTE PTR [r9 + rsi]"); // load the line's last byte + emitter.instruction("cmp dl, 0x0A"); // is the line terminated by a line feed? + emitter.instruction(&format!("jne __rt_file_{}_keep_eol", site)); // nothing to trim without a line feed + emitter.instruction("mov rcx, rsi"); // drop the trailing line feed + emitter.instruction("test rcx, rcx"); // did dropping the line feed leave an empty line? + emitter.instruction(&format!("jz __rt_file_{}_keep_eol", site)); // a lone line feed leaves an empty line + emitter.instruction("mov rsi, rcx"); // copy the trimmed length before probing the previous byte + emitter.instruction("sub rsi, 1"); // index of the byte before the dropped line feed + emitter.instruction("mov dl, BYTE PTR [r9 + rsi]"); // load that byte to detect a CRLF terminator + emitter.instruction("cmp dl, 0x0D"); // is the terminator a carriage return + line feed pair? + emitter.instruction(&format!("jne __rt_file_{}_keep_eol", site)); // a bare line feed needs no further trimming + emitter.instruction("mov rcx, rsi"); // drop the carriage return of a CRLF terminator + emitter.label(&format!("__rt_file_{}_keep_eol", site)); + emitter.instruction("test rax, 4"); // FILE_SKIP_EMPTY_LINES requested? + emitter.instruction(&format!("je __rt_file_{}_emit", site)); // keep every line when the flag is clear + emitter.instruction("test rcx, rcx"); // is the line empty after the trimming above? + emitter.instruction(&format!("jz __rt_file_{}_skip", site)); // suppress a line left empty by the trimming above + emitter.label(&format!("__rt_file_{}_emit", site)); +} diff --git a/src/codegen_support/runtime/io/file_get_contents_range.rs b/src/codegen_support/runtime/io/file_get_contents_range.rs new file mode 100644 index 0000000000..6b440d7bfd --- /dev/null +++ b/src/codegen_support/runtime/io/file_get_contents_range.rs @@ -0,0 +1,209 @@ +//! Purpose: +//! Emits `__rt_file_get_contents_range`, the runtime helper that applies PHP's +//! `file_get_contents()` `$offset`/`$length` window to bytes that have already been read. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. +//! - The EIR lowering of `file_get_contents()` in `crate::codegen::lower_inst::builtins::io`. +//! +//! Key details: +//! - The window is applied IN PLACE on an owned heap string: the kept bytes are shifted to the +//! front and the returned length shrinks. Nothing is reallocated, so the kept byte count is +//! bounded by the bytes that were actually read and a huge `$length` can never size a write. +//! - A negative `$offset` counts from the end. One that reaches before byte zero is php-src's +//! "Failed to seek to position N in the stream" warning plus `false`, so the buffer is released +//! and a null pointer is returned for `box_owned_string_or_false_result` to box as `false`. +//! - A null input pointer (the read already failed) passes straight through untouched, so the +//! original "Failed to open stream" warning stays the only diagnostic. + +use crate::codegen_support::abi; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// The message fragment php-src writes before the requested seek position. +const SEEK_FAILED_PREFIX: &str = "Warning: file_get_contents(): Failed to seek to position "; + +/// The message fragment php-src writes after the requested seek position. +const SEEK_FAILED_SUFFIX: &str = " in the stream\n"; + +/// The `.data` labels and bytes for the seek-failure warning, shared with the runtime data emitter. +/// +/// The emitter below derives every length immediate from this table, so the bytes emitted into +/// `.data` and the lengths passed to `__rt_concat` can never drift apart. +pub const FILE_GET_CONTENTS_SEEK_MESSAGES: &[(&str, &str)] = &[ + ("_diag_fgc_seek_prefix_msg", SEEK_FAILED_PREFIX), + ("_diag_fgc_seek_suffix_msg", SEEK_FAILED_SUFFIX), +]; + +/// Emits `__rt_file_get_contents_range` for the active target. +/// +/// ## ARM64 ABI +/// - **Input**: `x1` = owned bytes pointer (0 when the read failed), `x2` = byte count, +/// `x3` = `$offset`, `x4` = `$length`, `x5` = 1 when a `$length` was supplied and 0 when it was +/// omitted or `null` +/// - **Output**: `x1` = bytes pointer, `x2` = kept byte count (`x1` = 0 on a failed seek) +/// +/// ## x86_64 ABI +/// - **Input**: `rax` = owned bytes pointer, `rdx` = byte count, `rdi` = `$offset`, +/// `rsi` = `$length`, `rcx` = length-present flag +/// - **Output**: `rax` = bytes pointer, `rdx` = kept byte count +pub fn emit_file_get_contents_range(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_file_get_contents_range_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: file_get_contents_range ---"); + emitter.label_global("__rt_file_get_contents_range"); + + // Stack layout: [sp, #0] = requested $offset (needed for the warning after heap_free), + // [sp, #16] = saved x29/x30. + emitter.instruction("sub sp, sp, #32"); // reserve one spill slot plus the saved frame registers + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish the helper frame pointer + emitter.instruction("str x3, [sp, #0]"); // preserve the requested seek position for the failure message + emitter.instruction("cbz x1, __rt_fgc_range_return"); // a failed read is already PHP false and needs no window + + // -- resolve the requested start position against PHP's negative-offset rule -- + emitter.instruction("cmp x3, #0"); // is the requested offset counted from the end of the data? + emitter.instruction("add x9, x2, x3"); // compute the end-relative start position + emitter.instruction("csel x9, x9, x3, lt"); // x9 = start, end-relative for a negative offset and absolute otherwise + emitter.instruction("cmp x9, #0"); // did the requested position land before the first byte? + emitter.instruction("b.lt __rt_fgc_range_seek_failed"); // php-src reports an unreachable seek instead of clamping + emitter.instruction("cmp x9, x2"); // does the start position lie past the last byte? + emitter.instruction("csel x9, x9, x2, lt"); // clamp the start position to the end so an over-large offset yields "" + emitter.instruction("sub x10, x2, x9"); // x10 = bytes still available after the start position + + // -- bound the kept byte count by both $length and the available bytes -- + emitter.instruction("cmp x4, x10"); // compare the requested byte count with what is actually available + emitter.instruction("csel x11, x4, x10, lt"); // x11 = min($length, available) + emitter.instruction("cmp x5, #0"); // did the caller supply a $length at all? + emitter.instruction("csel x10, x11, x10, ne"); // an absent $length keeps every remaining byte + emitter.instruction("cmp x10, #0"); // could the bounded count still be negative? + emitter.instruction("csel x10, x10, xzr, gt"); // never keep a negative number of bytes + + // -- slide the kept bytes to the front of the owned buffer -- + emitter.instruction("cbz x9, __rt_fgc_range_trim"); // a zero start position already has the bytes in place + emitter.instruction("cbz x10, __rt_fgc_range_trim"); // an empty window has nothing to move + emitter.instruction("add x12, x1, x9"); // x12 = read cursor at the first kept byte + emitter.instruction("mov x13, x1"); // x13 = write cursor at the front of the buffer + emitter.instruction("mov x14, x10"); // x14 = number of bytes still to move + emitter.label("__rt_fgc_range_move"); + emitter.instruction("ldrb w15, [x12], #1"); // load the next kept byte and advance the read cursor + emitter.instruction("strb w15, [x13], #1"); // store it at the front and advance the write cursor + emitter.instruction("subs x14, x14, #1"); // one fewer byte left to move + emitter.instruction("b.ne __rt_fgc_range_move"); // keep moving until the whole window has slid forward + + emitter.label("__rt_fgc_range_trim"); + emitter.instruction("mov x2, x10"); // publish the kept byte count as the string length + emitter.instruction("b __rt_fgc_range_return"); // the trimmed buffer is the result + + // -- php-src's unreachable-seek warning, then PHP false -- + emitter.label("__rt_fgc_range_seek_failed"); + emitter.instruction("mov x0, x1"); // release the fully read buffer the caller will never see + emitter.instruction("bl __rt_heap_free"); // return the read storage before answering with false + emitter.instruction("ldr x0, [sp, #0]"); // reload the requested seek position for the message + emitter.instruction("bl __rt_itoa"); // render the requested position as decimal digits + emitter.instruction("mov x3, x1"); // move the digits into the concat right operand + emitter.instruction("mov x4, x2"); // move the digit count into the concat right operand + abi::emit_symbol_address(emitter, "x1", FILE_GET_CONTENTS_SEEK_MESSAGES[0].0); + emitter.instruction(&format!("mov x2, #{}", SEEK_FAILED_PREFIX.len())); // pass the warning prefix byte length to the concat helper + emitter.instruction("bl __rt_concat"); // build "…Failed to seek to position " + abi::emit_symbol_address(emitter, "x3", FILE_GET_CONTENTS_SEEK_MESSAGES[1].0); + emitter.instruction(&format!("mov x4, #{}", SEEK_FAILED_SUFFIX.len())); // pass the warning suffix byte length to the concat helper + emitter.instruction("bl __rt_concat"); // append " in the stream\n" to the warning text + emitter.instruction("bl __rt_diag_warning"); // emit or suppress the unreachable-seek warning + emitter.instruction("mov x1, #0"); // a null string pointer asks the caller's boxer for PHP false + emitter.instruction("mov x2, #0"); // clear the unused failure length + + emitter.label("__rt_fgc_range_return"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the windowed pointer/length pair +} + +/// Emits the x86_64 System V variant of `__rt_file_get_contents_range`. +/// +/// Mirrors the ARM64 logic register for register: the requested seek position is spilled because +/// `__rt_heap_free` and `__rt_itoa` clobber the caller-saved registers, and the kept byte count is +/// bounded by both `$length` and the bytes that remain after the start position before any store +/// happens. +fn emit_file_get_contents_range_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: file_get_contents_range ---"); + emitter.label_global("__rt_file_get_contents_range"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the window helper + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the spilled seek position + emitter.instruction("sub rsp, 16"); // reserve one aligned spill slot for the requested seek position + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the requested seek position for the failure message + emitter.instruction("test rax, rax"); // did the read already fail and answer with a null pointer? + emitter.instruction("jz __rt_fgc_range_return_x86"); // a failed read is already PHP false and needs no window + + // -- resolve the requested start position against PHP's negative-offset rule -- + emitter.instruction("mov r8, rdx"); // stage the byte count for the end-relative start computation + emitter.instruction("add r8, rdi"); // compute the end-relative start position + emitter.instruction("test rdi, rdi"); // is the requested offset counted from the end of the data? + emitter.instruction("cmovns r8, rdi"); // r8 = start, absolute for a non-negative offset + emitter.instruction("test r8, r8"); // did the requested position land before the first byte? + emitter.instruction("js __rt_fgc_range_seek_failed_x86"); // php-src reports an unreachable seek instead of clamping + emitter.instruction("cmp r8, rdx"); // does the start position lie past the last byte? + emitter.instruction("cmovg r8, rdx"); // clamp the start position to the end so an over-large offset yields "" + emitter.instruction("mov r9, rdx"); // stage the byte count for the available-bytes computation + emitter.instruction("sub r9, r8"); // r9 = bytes still available after the start position + + // -- bound the kept byte count by both $length and the available bytes -- + emitter.instruction("mov r10, r9"); // default the kept byte count to every remaining byte + emitter.instruction("cmp rsi, r9"); // compare the requested byte count with what is actually available + emitter.instruction("cmovl r10, rsi"); // r10 = min($length, available) + emitter.instruction("test rcx, rcx"); // did the caller supply a $length at all? + emitter.instruction("cmovz r10, r9"); // an absent $length keeps every remaining byte + emitter.instruction("xor r11d, r11d"); // materialize zero as the floor for the kept byte count + emitter.instruction("cmp r10, 0"); // could the bounded count still be negative? + emitter.instruction("cmovl r10, r11"); // never keep a negative number of bytes + + // -- slide the kept bytes to the front of the owned buffer -- + emitter.instruction("test r8, r8"); // is the window already at the front of the buffer? + emitter.instruction("jz __rt_fgc_range_trim_x86"); // a zero start position already has the bytes in place + emitter.instruction("test r10, r10"); // does the window keep any bytes at all? + emitter.instruction("jz __rt_fgc_range_trim_x86"); // an empty window has nothing to move + emitter.instruction("mov rsi, rax"); // seed the read cursor from the owned buffer base + emitter.instruction("add rsi, r8"); // advance the read cursor to the first kept byte + emitter.instruction("mov rdi, rax"); // seed the write cursor at the front of the owned buffer + emitter.instruction("mov rcx, r10"); // seed the move counter from the kept byte count + emitter.label("__rt_fgc_range_move_x86"); + emitter.instruction("mov r11b, BYTE PTR [rsi]"); // load the next kept byte from the read cursor + emitter.instruction("mov BYTE PTR [rdi], r11b"); // store it at the write cursor near the front of the buffer + emitter.instruction("add rsi, 1"); // advance the read cursor past the copied byte + emitter.instruction("add rdi, 1"); // advance the write cursor past the copied byte + emitter.instruction("sub rcx, 1"); // one fewer byte left to move + emitter.instruction("jnz __rt_fgc_range_move_x86"); // keep moving until the whole window has slid forward + + emitter.label("__rt_fgc_range_trim_x86"); + emitter.instruction("mov rdx, r10"); // publish the kept byte count as the string length + emitter.instruction("jmp __rt_fgc_range_return_x86"); // the trimmed buffer is the result + + // -- php-src's unreachable-seek warning, then PHP false -- + emitter.label("__rt_fgc_range_seek_failed_x86"); + emitter.instruction("call __rt_heap_free"); // release the fully read buffer the caller will never see + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the requested seek position for the message + emitter.instruction("call __rt_itoa"); // render the requested position as decimal digits + emitter.instruction("mov rdi, rax"); // move the digits into the concat right operand + emitter.instruction("mov rsi, rdx"); // move the digit count into the concat right operand + abi::emit_symbol_address(emitter, "rax", FILE_GET_CONTENTS_SEEK_MESSAGES[0].0); + emitter.instruction(&format!("mov rdx, {}", SEEK_FAILED_PREFIX.len())); // pass the warning prefix byte length to the concat helper + emitter.instruction("call __rt_concat"); // build "…Failed to seek to position " + abi::emit_symbol_address(emitter, "rdi", FILE_GET_CONTENTS_SEEK_MESSAGES[1].0); + emitter.instruction(&format!("mov rsi, {}", SEEK_FAILED_SUFFIX.len())); // pass the warning suffix byte length to the concat helper + emitter.instruction("call __rt_concat"); // append " in the stream\n" to the warning text + emitter.instruction("mov rdi, rax"); // pass the warning text pointer to the diagnostic helper + emitter.instruction("mov rsi, rdx"); // pass the warning text length to the diagnostic helper + emitter.instruction("call __rt_diag_warning"); // emit or suppress the unreachable-seek warning + emitter.instruction("xor eax, eax"); // a null string pointer asks the caller's boxer for PHP false + emitter.instruction("xor edx, edx"); // clear the unused failure length + + emitter.label("__rt_fgc_range_return_x86"); + emitter.instruction("add rsp, 16"); // release the spill slot used for the seek position + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the windowed pointer/length pair +} diff --git a/src/codegen_support/runtime/io/fread.rs b/src/codegen_support/runtime/io/fread.rs index 3a1533cf87..cf720ad21b 100644 --- a/src/codegen_support/runtime/io/fread.rs +++ b/src/codegen_support/runtime/io/fread.rs @@ -7,14 +7,19 @@ //! //! Key details: //! - I/O helpers bridge PHP strings, resources, descriptors, and libc calls while returning runtime arrays or pointer/length strings. +//! - The destination window is reserved through `__rt_concat_reserve` for the FULL requested +//! read length before the syscall, so an attacker-sized `fread($f, 100000)` lands in owned +//! heap storage instead of running past the 64 KiB concat scratch into the stream-handle, +//! exception and heap globals that follow it in BSS. use crate::codegen_support::{emit::Emitter, platform::Arch}; use crate::codegen_support::abi; /// Emits the `__rt_fread` runtime helper for reading bytes from a file descriptor. /// -/// On ARM64: reads into the concat buffer, updates `_concat_off`, sets `_eof_flags[fd]` on EOF, -/// and returns (pointer, byte_count) in x1:x2. +/// On ARM64: reads into storage reserved by `__rt_concat_reserve`, publishes the bytes read +/// through `__rt_concat_publish`, sets `_eof_flags[fd]` on EOF, and returns +/// (pointer, byte_count) in x1:x2. /// /// On x86_64: same semantics but uses libc `read()` and returns (pointer, byte_count) in rax:rdx. /// @@ -23,11 +28,12 @@ use crate::codegen_support::abi; /// - x1/rsi: number of bytes to read /// /// # Outputs -/// - x1/x86_64 rax: pointer to bytes in concat buffer (borrowed, not owned) +/// - x1/x86_64 rax: pointer to the bytes read. Concat-scratch-backed (borrowed) when the +/// requested length still fits the shared 64 KiB buffer, heap-backed otherwise. /// - x2/rdx: actual bytes read (0 on EOF/error) /// /// # Side effects -/// - Advances `_concat_off` by actual bytes read. +/// - Advances `_concat_off` by the actual bytes read, but only for scratch-backed results. /// - Sets `_eof_flags[fd] = 1` when the stream is exhausted. pub fn emit_fread(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -56,11 +62,19 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #0]"); // save file descriptor emitter.instruction("str x1, [sp, #8]"); // save requested read length - // -- get concat_buf write position -- + // -- reserve a destination sized for the whole requested read -- + emitter.instruction("cmp x1, #1"); // does the caller actually request at least one byte? + emitter.instruction("b.lt __rt_fread_dest_scratch"); // non-positive requests write nothing, so keep the current scratch tail + emitter.instruction("mov x0, x1"); // request storage for the full requested read length + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the incoming bytes + emitter.instruction("mov x12, x0"); // destination pointer for the read + emitter.instruction("b __rt_fread_dest_ready"); // the destination window is reserved + emitter.label("__rt_fread_dest_scratch"); crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); emitter.instruction("ldr x10, [x9]"); // load current write offset crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); emitter.instruction("add x12, x11, x10"); // compute write pointer: buf + offset + emitter.label("__rt_fread_dest_ready"); emitter.instruction("str x12, [sp, #16]"); // save start pointer for return value // -- TLS dispatch: route through elephc_tls_read when fd has an @@ -70,7 +84,7 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.instruction("ldr x14, [x13, x0, lsl #3]"); // _tls_sessions[fd] handle (0 = plain TCP) emitter.instruction("cbz x14, __rt_fread_do_syscall"); // no TLS attached → fall through to read syscall emitter.instruction("mov x0, x14"); // handle as first arg - emitter.instruction("mov x1, x12"); // buf ptr + emitter.instruction("ldr x1, [sp, #16]"); // buf ptr emitter.instruction("ldr x2, [sp, #8]"); // len crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_elephc_tls_read_fn"); emitter.instruction("ldr x9, [x9]"); // load elephc_tls_read entry pointer @@ -83,7 +97,7 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.label("__rt_fread_do_syscall"); // -- perform read syscall -- emitter.instruction("ldr x0, [sp, #0]"); // fd for read syscall - emitter.instruction("mov x1, x12"); // buffer pointer for read + emitter.instruction("ldr x1, [sp, #16]"); // buffer pointer for read emitter.instruction("ldr x2, [sp, #8]"); // number of bytes to read emitter.syscall(3); if emitter.platform.needs_cmp_before_error_branch() { @@ -100,12 +114,11 @@ pub fn emit_fread(emitter: &mut Emitter) { emitter.instruction("b __rt_fread_mark_eof"); // mark the stream as exhausted after a read failure emitter.label("__rt_fread_read_ok"); - // -- update concat_off by actual bytes read -- + // -- publish the bytes actually read into the reserved destination -- emitter.instruction("str x0, [sp, #24]"); // save actual bytes read - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - emitter.instruction("add x10, x10, x0"); // advance offset by bytes read - emitter.instruction("str x10, [x9]"); // store updated offset + emitter.instruction("ldr x1, [sp, #16]"); // reload the reserved destination pointer + emitter.instruction("mov x2, x0"); // pass the number of bytes actually read + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed reads // -- set eof flag if read returned 0 -- emitter.instruction("ldr x0, [sp, #24]"); // reload bytes read @@ -168,10 +181,18 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // preserve the file descriptor across the concat-buffer address computation and libc read() call emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // preserve the requested byte count across the concat-buffer address computation and libc read() call + emitter.instruction("cmp rsi, 1"); // does the caller actually request at least one byte? + emitter.instruction("jl __rt_fread_dest_scratch_x86"); // non-positive requests write nothing, so keep the current scratch tail + emitter.instruction("mov rax, rsi"); // request storage for the full requested read length + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the incoming bytes + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the reserved destination pointer for the final elephc string result + emitter.instruction("jmp __rt_fread_dest_ready_x86"); // the destination window is reserved + emitter.label("__rt_fread_dest_scratch_x86"); abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // load the current concat-buffer absolute offset before appending the fread() result abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // materialize the concat-buffer base address once for the x86_64 fread() helper emitter.instruction("lea rax, [r11 + r10]"); // compute the start pointer for the bytes that libc read() will append emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // preserve the concat-buffer start pointer for the final elephc string result + emitter.label("__rt_fread_dest_ready_x86"); // -- TLS dispatch: route through elephc_tls_read when fd has an // attached session (Phase 11 B3). -- @@ -199,11 +220,9 @@ fn emit_fread_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_fread_eof_x86"); // zero-byte read means real EOF emitter.label("__rt_fread_read_ok_x86"); - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // reload the previous concat-buffer absolute offset before publishing the fread() append - emitter.instruction("add r10, rax"); // advance the concat-buffer offset by the number of bytes libc read() returned - abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // publish the updated concat-buffer offset for later string appenders emitter.instruction("mov rdx, rax"); // return the successful byte count in the x86_64 elephc string-length result register - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the concat-buffer start pointer in the x86_64 elephc string-pointer result register + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the reserved start pointer in the x86_64 elephc string-pointer result register + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed reads emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the file descriptor for the read-filter lookup abi::emit_symbol_address(emitter, "r11", "_stream_read_filters"); // materialize the read-filter table base emitter.instruction("movzx ecx, BYTE PTR [r11 + r10]"); // read filter id for this descriptor diff --git a/src/codegen_support/runtime/io/mod.rs b/src/codegen_support/runtime/io/mod.rs index 53faa64df8..2d8d2b338a 100644 --- a/src/codegen_support/runtime/io/mod.rs +++ b/src/codegen_support/runtime/io/mod.rs @@ -18,6 +18,7 @@ mod fgetcsv; mod fgets; mod file; mod file_get_contents; +mod file_get_contents_range; mod file_get_contents_url; mod file_put_contents; mod fd_write; @@ -136,6 +137,9 @@ pub(crate) use fgetcsv::emit_fgetcsv; pub(crate) use fgets::emit_fgets; pub(crate) use file::emit_file; pub(crate) use file_get_contents::emit_file_get_contents; +pub(crate) use file_get_contents_range::{ + emit_file_get_contents_range, FILE_GET_CONTENTS_SEEK_MESSAGES, +}; pub(crate) use file_get_contents_url::emit_file_get_contents_url; pub(crate) use fd_write::emit_fd_write; pub(crate) use file_put_contents::emit_file_put_contents; diff --git a/src/codegen_support/runtime/io/print_r_walk.rs b/src/codegen_support/runtime/io/print_r_walk.rs index 7343b51384..9f90a8e934 100644 --- a/src/codegen_support/runtime/io/print_r_walk.rs +++ b/src/codegen_support/runtime/io/print_r_walk.rs @@ -25,8 +25,10 @@ //! entries, 0 for a top-level Mixed value). //! - Scalars render PHP-style with no type wrapper: int/float as decimals, //! strings raw, bool true as `1` and bool false / null as the empty string. -//! - Nested objects (tag 6) are rendered as the bare `Array` header only; full -//! `ClassName Object` dumps need class metadata the runtime walker lacks. +//! - Nested objects (tag 6) hand off to `__rt_print_r_object` in +//! `codegen_support::runtime::objects::print_r_object`, which owns the whole +//! `ClassName Object\n(\n ... )\n` frame (and the enum header) the same way the +//! tag-4/5 branches own the array frame. //! - The AArch64 path is shared by macOS and Linux ARM64 (`emitter.syscall(4)` //! maps to the platform write number); the `_linux_x86_64` paths are SysV. @@ -308,7 +310,9 @@ pub fn emit_print_r_value(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_pr_val_arr"); // recurse into the indexed walker emitter.instruction("cmp x0, #5"); // tag 5 = hash emitter.instruction("b.eq __rt_pr_val_hash"); // recurse into the hash walker - emitter.instruction("b __rt_pr_val_done"); // tag 6 object / 8 null → render nothing + emitter.instruction("cmp x0, #6"); // tag 6 = object + emitter.instruction("b.eq __rt_pr_val_obj"); // recurse into the object walker + emitter.instruction("b __rt_pr_val_done"); // tag 8 null → render nothing emitter.label("__rt_pr_val_int"); emitter.instruction("ldr x0, [sp, #0]"); // reload the integer payload @@ -354,6 +358,13 @@ pub fn emit_print_r_value(emitter: &mut Emitter) { emitter.instruction("bl __rt_print_r_hash"); // recurse into the hash walker emitter.instruction("b __rt_pr_val_done"); // value rendered + emitter.label("__rt_pr_val_obj"); + emitter.instruction("ldr x0, [sp, #0]"); // nested object pointer + emitter.instruction("cbz x0, __rt_pr_val_done"); // defensive: a null instance renders nothing + emitter.instruction("ldr x1, [sp, #16]"); // base = the nested paren indent + emitter.instruction("bl __rt_print_r_object"); // recurse into the object walker + emitter.instruction("b __rt_pr_val_done"); // value rendered + emitter.label("__rt_pr_val_mixed"); emitter.instruction("ldr x0, [sp, #0]"); // boxed Mixed cell pointer emitter.instruction("bl __rt_mixed_unbox"); // x0=inner tag, x1=lo, x2=hi @@ -394,7 +405,9 @@ fn emit_print_r_value_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_pr_val_arr_x86"); // recurse into the indexed walker emitter.instruction("cmp rax, 5"); // tag 5 = hash emitter.instruction("je __rt_pr_val_hash_x86"); // recurse into the hash walker - emitter.instruction("jmp __rt_pr_val_done_x86"); // tag 6 object / 8 null → render nothing + emitter.instruction("cmp rax, 6"); // tag 6 = object + emitter.instruction("je __rt_pr_val_obj_x86"); // recurse into the object walker + emitter.instruction("jmp __rt_pr_val_done_x86"); // tag 8 null → render nothing emitter.label("__rt_pr_val_int_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the integer payload @@ -443,6 +456,14 @@ fn emit_print_r_value_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_print_r_hash"); // recurse into the hash walker emitter.instruction("jmp __rt_pr_val_done_x86"); // value rendered + emitter.label("__rt_pr_val_obj_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // nested object pointer + emitter.instruction("test rdi, rdi"); // defensive null-instance check + emitter.instruction("jz __rt_pr_val_done_x86"); // a null instance renders nothing + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // base = the nested paren indent + emitter.instruction("call __rt_print_r_object"); // recurse into the object walker + emitter.instruction("jmp __rt_pr_val_done_x86"); // value rendered + emitter.label("__rt_pr_val_mixed_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // boxed Mixed cell pointer → RAX emitter.instruction("call __rt_mixed_unbox"); // rax=inner tag, rdi=lo, rdx=hi diff --git a/src/codegen_support/runtime/io/stream_get_contents.rs b/src/codegen_support/runtime/io/stream_get_contents.rs index bca376bfce..d8044ee2f4 100644 --- a/src/codegen_support/runtime/io/stream_get_contents.rs +++ b/src/codegen_support/runtime/io/stream_get_contents.rs @@ -7,19 +7,28 @@ //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. //! //! Key details: -//! - The result string is a borrowed slice of `_concat_buf`, matching `__rt_fread`. +//! - The accumulation buffer is reserved through `__rt_concat_reserve` and enlarged through +//! `__rt_concat_grow`, so a stream larger than the 64 KiB concat scratch produces an owned +//! heap string instead of running off the end of `_concat_buf` into the adjacent BSS globals. +//! - The reservation claims its whole window with `__rt_concat_publish(buf, capacity)` before +//! the first `__rt_fread`, so each chunk reservation lands *after* the accumulated result; +//! `__rt_concat_publish(chunk, 0)` then hands the chunk window straight back. //! - The read-all loop uses `__rt_fread` so TLS sessions, filters, and wrapper //! reads share one I/O dispatch path. -use crate::codegen_support::abi::emit_symbol_address; use crate::codegen_support::{emit::Emitter, platform::Arch}; -use crate::codegen_support::abi; + +/// Initial accumulation capacity, in bytes. Two read chunks wide so the first `__rt_fread` +/// never has to grow, and small enough that short streams still stay in concat scratch. +const INITIAL_CAPACITY: usize = 8192; +/// Bytes requested from `__rt_fread` per iteration. +const READ_CHUNK: usize = 4096; /// Emits the read-all stream helper. /// /// Input: `x0 = fd`. Output: `x1 = string pointer`, `x2 = total bytes read`. -/// The helper loops through `__rt_fread`, compacts each returned chunk into -/// `_concat_buf`, and stops when EOF or an empty read is produced. +/// The helper loops through `__rt_fread`, copies each returned chunk into a reserved +/// accumulation buffer that grows on demand, and stops when EOF or an empty read is produced. pub fn emit_stream_get_contents(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_stream_get_contents_linux_x86_64(emitter); @@ -31,21 +40,23 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.label_global("__rt_stream_get_contents"); // -- set up stack frame -- - emitter.instruction("sub sp, sp, #80"); // allocate locals plus saved frame pointer and return address - emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #64"); // establish the helper frame pointer + emitter.instruction("sub sp, sp, #96"); // allocate locals plus saved frame pointer and return address + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the source file descriptor - // -- record the start of the result inside the concat buffer -- - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load the current concat-buffer offset - emitter.instruction("str x10, [sp, #8]"); // save the result start offset - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // compute the result start pointer - emitter.instruction("str x12, [sp, #16]"); // save the result start pointer + // -- reserve the accumulation buffer and claim its whole window -- + emitter.instruction(&format!("mov x9, #{}", INITIAL_CAPACITY)); // start from the initial accumulation capacity + emitter.instruction("str x9, [sp, #48]"); // save the current accumulation capacity + emitter.instruction("mov x0, x9"); // request the initial capacity from the reservation front end + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the accumulated result + emitter.instruction("str x0, [sp, #16]"); // save the accumulation buffer pointer + emitter.instruction("mov x1, x0"); // publish the reservation start pointer + emitter.instruction("ldr x2, [sp, #48]"); // publish the full reserved capacity + emitter.instruction("bl __rt_concat_publish"); // claim the whole window so each chunk read appends after it emitter.instruction("str xzr, [sp, #24]"); // initialize the running byte total to zero - // -- read 4096-byte chunks through fread until EOF -- + // -- read fixed-size chunks through fread until EOF -- emitter.label("__rt_stream_get_contents_loop"); emitter.instruction("ldr x0, [sp, #0]"); // reload the source file descriptor emitter.instruction("mov w11, #0x4000"); // high half of USER_WRAPPER_FD_BASE @@ -55,28 +66,42 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("bl __rt_feof"); // wrapper: check stream_eof before reading emitter.instruction("cbnz x0, __rt_stream_get_contents_done"); // wrapper EOF means no extra stream_read call emitter.label("__rt_stream_get_contents_after_feof"); + + // -- make room for one more chunk before asking fread for it -- emitter.instruction("ldr x9, [sp, #24]"); // running result length - emitter.instruction("ldr x12, [sp, #8]"); // result start offset - emitter.instruction("add x12, x12, x9"); // compact append offset = start + total - emit_symbol_address(emitter, "x13", "_concat_off"); - emitter.instruction("str x12, [x13]"); // make __rt_fread append at the compact tail + emitter.instruction(&format!("add x9, x9, #{}", READ_CHUNK)); // capacity needed once the next chunk lands + emitter.instruction("ldr x10, [sp, #48]"); // current accumulation capacity + emitter.instruction("cmp x9, x10"); // does the next chunk still fit the current reservation? + emitter.instruction("b.ls __rt_stream_get_contents_have_room"); // no growth needed for this iteration + emitter.instruction("lsl x10, x10, #1"); // double the accumulation capacity + emitter.instruction("cmp x10, x9"); // is the doubled capacity already large enough? + emitter.instruction("csel x10, x10, x9, hi"); // keep whichever capacity is larger + emitter.instruction("str x10, [sp, #48]"); // save the grown accumulation capacity + emitter.instruction("ldr x1, [sp, #16]"); // old accumulation buffer pointer + emitter.instruction("mov x2, #0"); // release the whole claimed window + emitter.instruction("bl __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("ldr x0, [sp, #16]"); // old accumulation buffer pointer + emitter.instruction("ldr x1, [sp, #24]"); // bytes accumulated so far must survive the move + emitter.instruction("ldr x2, [sp, #48]"); // grown accumulation capacity + emitter.instruction("bl __rt_concat_grow"); // move the accumulated bytes into a larger owned buffer + emitter.instruction("str x0, [sp, #16]"); // save the grown accumulation buffer pointer + emitter.label("__rt_stream_get_contents_have_room"); + emitter.instruction("ldr x0, [sp, #0]"); // reload fd for __rt_fread - emitter.instruction("mov x1, #4096"); // request one read-all chunk + emitter.instruction(&format!("mov x1, #{}", READ_CHUNK)); // request one read-all chunk emitter.instruction("bl __rt_fread"); // x1=chunk ptr, x2=chunk len emitter.instruction("cbz x2, __rt_stream_get_contents_release_done"); // empty read stops the read-all loop emitter.instruction("str x1, [sp, #32]"); // save chunk pointer across the copy emitter.instruction("str x2, [sp, #40]"); // save chunk length across the copy - emitter.instruction("ldr x9, [sp, #24]"); // reload running result length after __rt_fread - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("ldr x12, [sp, #8]"); // result start offset - emitter.instruction("add x11, x11, x12"); // result base pointer - emitter.instruction("add x11, x11, x9"); // destination = result base + total + emitter.instruction("ldr x11, [sp, #16]"); // accumulation buffer base pointer + emitter.instruction("ldr x9, [sp, #24]"); // running result length before this chunk + emitter.instruction("add x11, x11, x9"); // destination = accumulation base + total emitter.instruction("mov x12, #0"); // byte-copy index emitter.label("__rt_stream_get_contents_copy"); emitter.instruction("cmp x12, x2"); // copied this whole chunk? emitter.instruction("b.ge __rt_stream_get_contents_copy_done"); // leave the copy loop once chunk bytes are copied emitter.instruction("ldrb w13, [x1, x12]"); // load the next chunk byte - emitter.instruction("strb w13, [x11, x12]"); // store it at the compact destination + emitter.instruction("strb w13, [x11, x12]"); // store it at the accumulation destination emitter.instruction("add x12, x12, #1"); // advance the copy index emitter.instruction("b __rt_stream_get_contents_copy"); // copy the next byte emitter.label("__rt_stream_get_contents_copy_done"); @@ -84,24 +109,27 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { emitter.instruction("ldr x10, [sp, #40]"); // copied chunk length emitter.instruction("add x9, x9, x10"); // include the copied chunk in the total emitter.instruction("str x9, [sp, #24]"); // store the updated result length - emitter.instruction("ldr x12, [sp, #8]"); // result start offset - emitter.instruction("add x12, x12, x9"); // compact tail offset after this chunk - emit_symbol_address(emitter, "x13", "_concat_off"); - emitter.instruction("str x12, [x13]"); // publish the compacted concat-buffer tail + emitter.instruction("ldr x1, [sp, #32]"); // reload the chunk pointer + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand this chunk's scratch window back for the next read emitter.instruction("ldr x0, [sp, #32]"); // reload the chunk pointer emitter.instruction("bl __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("b __rt_stream_get_contents_loop"); // read the next chunk // -- release the terminal empty chunk and return the accumulated string -- emitter.label("__rt_stream_get_contents_release_done"); - emitter.instruction("mov x0, x1"); // final empty chunk pointer + emitter.instruction("str x1, [sp, #32]"); // save the final empty chunk pointer + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand the terminal chunk's scratch window back + emitter.instruction("ldr x0, [sp, #32]"); // final empty chunk pointer emitter.instruction("bl __rt_decref_any"); // release it if it is heap-backed emitter.label("__rt_stream_get_contents_done"); - emitter.instruction("ldr x1, [sp, #16]"); // return the result start pointer + emitter.instruction("ldr x1, [sp, #16]"); // return the accumulation buffer pointer emitter.instruction("ldr x2, [sp, #24]"); // return the accumulated result length - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // release the helper frame - emitter.instruction("ret"); // return the accumulated string slice + emitter.instruction("bl __rt_concat_publish"); // shrink the claimed window down to the bytes actually accumulated + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the accumulated string emit_stream_get_contents_bounded_aarch64(emitter); } @@ -109,24 +137,33 @@ pub fn emit_stream_get_contents(emitter: &mut Emitter) { /// Emits the AArch64 bounded stream_get_contents helper. /// /// Input: `x0 = fd`, `x1 = max bytes`. Output: `x1 = ptr`, `x2 = len`. -/// The loop calls `__rt_fread` repeatedly, compacts each returned chunk into -/// `_concat_buf`, and stops at the requested byte count or EOF. +/// The loop calls `__rt_fread` repeatedly, copies each returned chunk into a reserved +/// accumulation buffer that grows on demand (never beyond the requested cap), and stops +/// at the requested byte count or EOF. fn emit_stream_get_contents_bounded_aarch64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: stream_get_contents_bounded ---"); emitter.label_global("__rt_stream_get_contents_bounded"); - emitter.instruction("sub sp, sp, #80"); // allocate locals plus saved frame pointer and return address - emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #64"); // establish the helper frame pointer + emitter.instruction("sub sp, sp, #96"); // allocate locals plus saved frame pointer and return address + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the source descriptor emitter.instruction("str x1, [sp, #8]"); // save the requested byte cap - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // snapshot the concat-buffer start offset - emitter.instruction("str x10, [sp, #16]"); // save the result start offset - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // compute the result start pointer - emitter.instruction("str x12, [sp, #24]"); // save the result start pointer + + // -- reserve min(cap, initial capacity) and claim the whole window -- + emitter.instruction(&format!("mov x9, #{}", INITIAL_CAPACITY)); // start from the initial accumulation capacity + emitter.instruction("cmp x1, x9"); // is the requested cap smaller than the initial capacity? + emitter.instruction("csel x9, x1, x9, lt"); // never reserve more than the caller asked for + emitter.instruction("cmp x9, #0"); // a non-positive cap reserves nothing at all + emitter.instruction("csel x9, xzr, x9, lt"); // clamp a negative cap to a zero-byte reservation + emitter.instruction("str x9, [sp, #56]"); // save the current accumulation capacity + emitter.instruction("mov x0, x9"); // request the initial capacity from the reservation front end + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the accumulated result + emitter.instruction("str x0, [sp, #24]"); // save the accumulation buffer pointer + emitter.instruction("mov x1, x0"); // publish the reservation start pointer + emitter.instruction("ldr x2, [sp, #56]"); // publish the full reserved capacity + emitter.instruction("bl __rt_concat_publish"); // claim the whole window so each chunk read appends after it emitter.instruction("str xzr, [sp, #32]"); // running result length = 0 emitter.label("__rt_stream_get_contents_bounded_loop"); @@ -142,16 +179,39 @@ fn emit_stream_get_contents_bounded_aarch64(emitter: &mut Emitter) { emitter.instruction("bl __rt_feof"); // wrapper: check stream_eof before reading emitter.instruction("cbnz x0, __rt_stream_get_contents_bounded_done"); // wrapper EOF means no extra stream_read call emitter.label("__rt_stream_get_contents_bounded_after_feof"); + + // -- make room for one more (cap-clamped) chunk before asking fread for it -- + emitter.instruction("ldr x9, [sp, #32]"); // running result length + emitter.instruction(&format!("add x9, x9, #{}", READ_CHUNK)); // capacity needed once the next chunk lands + emitter.instruction("ldr x10, [sp, #8]"); // requested byte cap + emitter.instruction("cmp x9, x10"); // would that exceed the caller's cap? + emitter.instruction("csel x9, x9, x10, lt"); // never grow the reservation past the requested cap + emitter.instruction("ldr x10, [sp, #56]"); // current accumulation capacity + emitter.instruction("cmp x9, x10"); // does the next chunk still fit the current reservation? + emitter.instruction("b.ls __rt_stream_get_contents_bounded_have_room"); // no growth needed for this iteration + emitter.instruction("lsl x10, x10, #1"); // double the accumulation capacity + emitter.instruction("cmp x10, x9"); // is the doubled capacity already large enough? + emitter.instruction("csel x10, x10, x9, hi"); // keep whichever capacity is larger + emitter.instruction("ldr x11, [sp, #8]"); // requested byte cap + emitter.instruction("cmp x10, x11"); // did doubling overshoot the caller's cap? + emitter.instruction("csel x10, x10, x11, lt"); // clamp the grown capacity to the requested cap + emitter.instruction("str x10, [sp, #56]"); // save the grown accumulation capacity + emitter.instruction("ldr x1, [sp, #24]"); // old accumulation buffer pointer + emitter.instruction("mov x2, #0"); // release the whole claimed window + emitter.instruction("bl __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("ldr x0, [sp, #24]"); // old accumulation buffer pointer + emitter.instruction("ldr x1, [sp, #32]"); // bytes accumulated so far must survive the move + emitter.instruction("ldr x2, [sp, #56]"); // grown accumulation capacity + emitter.instruction("bl __rt_concat_grow"); // move the accumulated bytes into a larger owned buffer + emitter.instruction("str x0, [sp, #24]"); // save the grown accumulation buffer pointer + emitter.label("__rt_stream_get_contents_bounded_have_room"); + emitter.instruction("ldr x9, [sp, #32]"); // running result length emitter.instruction("ldr x10, [sp, #8]"); // requested byte cap emitter.instruction("sub x1, x10, x9"); // remaining bytes needed - emitter.instruction("mov x11, #4096"); // maximum chunk request + emitter.instruction(&format!("mov x11, #{}", READ_CHUNK)); // maximum chunk request emitter.instruction("cmp x1, x11"); // is the remaining cap smaller than the chunk size? - emitter.instruction("csel x1, x1, x11, lt"); // request min(remaining, 4096) - emitter.instruction("ldr x12, [sp, #16]"); // result start offset - emitter.instruction("add x12, x12, x9"); // compact append offset = start + total - emit_symbol_address(emitter, "x13", "_concat_off"); - emitter.instruction("str x12, [x13]"); // make __rt_fread append at the compact tail + emitter.instruction("csel x1, x1, x11, lt"); // request min(remaining, chunk size) emitter.instruction("ldr x0, [sp, #0]"); // reload fd for __rt_fread emitter.instruction("bl __rt_fread"); // x1=chunk ptr, x2=chunk len emitter.instruction("cbz x2, __rt_stream_get_contents_bounded_release_done"); // empty read stops the bounded loop @@ -162,16 +222,14 @@ fn emit_stream_get_contents_bounded_aarch64(emitter: &mut Emitter) { emitter.instruction("csel x2, x2, x10, ls"); // clamp the chunk to the remaining cap emitter.instruction("str x1, [sp, #40]"); // save chunk pointer across the copy emitter.instruction("str x2, [sp, #48]"); // save chunk length across the copy - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("ldr x12, [sp, #16]"); // result start offset - emitter.instruction("add x11, x11, x12"); // result base pointer - emitter.instruction("add x11, x11, x9"); // destination = result base + total + emitter.instruction("ldr x11, [sp, #24]"); // accumulation buffer base pointer + emitter.instruction("add x11, x11, x9"); // destination = accumulation base + total emitter.instruction("mov x12, #0"); // byte-copy index emitter.label("__rt_stream_get_contents_bounded_copy"); emitter.instruction("cmp x12, x2"); // copied this whole chunk? emitter.instruction("b.ge __rt_stream_get_contents_bounded_copy_done"); // leave the copy loop once chunk bytes are copied emitter.instruction("ldrb w13, [x1, x12]"); // load the next chunk byte - emitter.instruction("strb w13, [x11, x12]"); // store it at the compact destination + emitter.instruction("strb w13, [x11, x12]"); // store it at the accumulation destination emitter.instruction("add x12, x12, #1"); // advance the copy index emitter.instruction("b __rt_stream_get_contents_bounded_copy"); // copy the next byte emitter.label("__rt_stream_get_contents_bounded_copy_done"); @@ -179,26 +237,32 @@ fn emit_stream_get_contents_bounded_aarch64(emitter: &mut Emitter) { emitter.instruction("ldr x10, [sp, #48]"); // copied chunk length emitter.instruction("add x9, x9, x10"); // include the copied chunk in the total emitter.instruction("str x9, [sp, #32]"); // store the updated result length - emitter.instruction("ldr x12, [sp, #16]"); // result start offset - emitter.instruction("add x12, x12, x9"); // compact tail offset after this chunk - emit_symbol_address(emitter, "x13", "_concat_off"); - emitter.instruction("str x12, [x13]"); // publish the compacted concat-buffer tail + emitter.instruction("ldr x1, [sp, #40]"); // reload the chunk pointer + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand this chunk's scratch window back for the next read emitter.instruction("ldr x0, [sp, #40]"); // reload the chunk pointer emitter.instruction("bl __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("b __rt_stream_get_contents_bounded_loop"); // read the next bounded chunk emitter.label("__rt_stream_get_contents_bounded_release_done"); - emitter.instruction("mov x0, x1"); // final empty chunk pointer + emitter.instruction("str x1, [sp, #40]"); // save the final empty chunk pointer + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand the terminal chunk's scratch window back + emitter.instruction("ldr x0, [sp, #40]"); // final empty chunk pointer emitter.instruction("bl __rt_decref_any"); // release it if it is heap-backed emitter.label("__rt_stream_get_contents_bounded_done"); - emitter.instruction("ldr x1, [sp, #24]"); // return the result start pointer + emitter.instruction("ldr x1, [sp, #24]"); // return the accumulation buffer pointer emitter.instruction("ldr x2, [sp, #32]"); // return the accumulated result length - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // release the helper frame - emitter.instruction("ret"); // return the bounded string slice + emitter.instruction("bl __rt_concat_publish"); // shrink the claimed window down to the bytes actually accumulated + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the bounded string } /// Emits the Linux x86_64 read-all stream helper. +/// +/// Input: `rdi = fd`. Output: `rax = ptr`, `rdx = len`. Mirrors the AArch64 accumulation +/// strategy: reserve, claim the window, grow on demand, publish the final length. fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: stream_get_contents ---"); @@ -208,11 +272,14 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base emitter.instruction("sub rsp, 64"); // reserve aligned locals for read-all accumulation emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source file descriptor - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // load the current concat-buffer offset - emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // save the result start offset - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // materialize the concat-buffer base address - emitter.instruction("lea rax, [r11 + r10]"); // compute the result start pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the result start pointer + + // -- reserve the accumulation buffer and claim its whole window -- + emitter.instruction(&format!("mov QWORD PTR [rbp - 56], {}", INITIAL_CAPACITY)); // start from the initial accumulation capacity + emitter.instruction(&format!("mov rax, {}", INITIAL_CAPACITY)); // request the initial capacity from the reservation front end + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the accumulated result + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the accumulation buffer pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // publish the full reserved capacity + emitter.instruction("call __rt_concat_publish"); // claim the whole window so each chunk read appends after it emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // initialize the running byte total to zero emitter.label("__rt_stream_get_contents_loop_x86"); @@ -224,49 +291,69 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rax, rax"); // did stream_eof report true? emitter.instruction("jnz __rt_stream_get_contents_done_x86"); // wrapper EOF means no extra stream_read call emitter.label("__rt_stream_get_contents_after_feof_x86"); + + // -- make room for one more chunk before asking fread for it -- emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // running result length - emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // result start offset - emitter.instruction("add r11, r8"); // compact append offset = start + total - abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // make __rt_fread append at the compact tail + emitter.instruction(&format!("add r8, {}", READ_CHUNK)); // capacity needed once the next chunk lands + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // current accumulation capacity + emitter.instruction("cmp r8, r9"); // does the next chunk still fit the current reservation? + emitter.instruction("jbe __rt_stream_get_contents_have_room_x86"); // no growth needed for this iteration + emitter.instruction("add r9, r9"); // double the accumulation capacity + emitter.instruction("cmp r9, r8"); // is the doubled capacity already large enough? + emitter.instruction("cmovb r9, r8"); // keep whichever capacity is larger + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the grown accumulation capacity + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // old accumulation buffer pointer + emitter.instruction("xor edx, edx"); // release the whole claimed window + emitter.instruction("call __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // old accumulation buffer pointer + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // bytes accumulated so far must survive the move + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // grown accumulation capacity + emitter.instruction("call __rt_concat_grow"); // move the accumulated bytes into a larger owned buffer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the grown accumulation buffer pointer + emitter.label("__rt_stream_get_contents_have_room_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload fd for __rt_fread - emitter.instruction("mov rsi, 4096"); // request one read-all chunk + emitter.instruction(&format!("mov rsi, {}", READ_CHUNK)); // request one read-all chunk emitter.instruction("call __rt_fread"); // rax=chunk ptr, rdx=chunk len emitter.instruction("test rdx, rdx"); // empty chunk? emitter.instruction("jz __rt_stream_get_contents_release_done_x86"); // empty read stops the read-all loop emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save chunk pointer across the copy emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save chunk length across the copy - emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // reload running result length after __rt_fread - abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // materialize the concat-buffer base - emitter.instruction("add r10, QWORD PTR [rbp - 16]"); // result base pointer - emitter.instruction("add r10, r8"); // destination = result base + total - emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // source chunk pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // accumulation buffer base pointer + emitter.instruction("add r10, QWORD PTR [rbp - 32]"); // destination = accumulation base + total + emitter.instruction("mov r11, rax"); // source chunk pointer emitter.instruction("xor rcx, rcx"); // byte-copy index emitter.label("__rt_stream_get_contents_copy_x86"); emitter.instruction("cmp rcx, QWORD PTR [rbp - 48]"); // copied this whole chunk? emitter.instruction("jge __rt_stream_get_contents_copy_done_x86"); // leave the copy loop once chunk bytes are copied emitter.instruction("mov r9b, BYTE PTR [r11 + rcx]"); // load the next chunk byte - emitter.instruction("mov BYTE PTR [r10 + rcx], r9b"); // store it at the compact destination + emitter.instruction("mov BYTE PTR [r10 + rcx], r9b"); // store it at the accumulation destination emitter.instruction("inc rcx"); // advance the copy index emitter.instruction("jmp __rt_stream_get_contents_copy_x86"); // copy the next byte emitter.label("__rt_stream_get_contents_copy_done_x86"); emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // running result length before this chunk emitter.instruction("add r8, QWORD PTR [rbp - 48]"); // include the copied chunk in the total emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // store the updated result length - emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // result start offset - emitter.instruction("add r11, r8"); // compact tail offset after this chunk - abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // publish the compacted concat-buffer tail + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the chunk pointer + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand this chunk's scratch window back for the next read emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the chunk pointer emitter.instruction("call __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("jmp __rt_stream_get_contents_loop_x86"); // read the next chunk emitter.label("__rt_stream_get_contents_release_done_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the final empty chunk pointer + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand the terminal chunk's scratch window back + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // final empty chunk pointer emitter.instruction("call __rt_decref_any"); // release the empty chunk if it is heap-backed emitter.label("__rt_stream_get_contents_done_x86"); - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the result start pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulation buffer pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // return the accumulated result length + emitter.instruction("call __rt_concat_publish"); // shrink the claimed window down to the bytes actually accumulated emitter.instruction("add rsp, 64"); // release the helper locals emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the accumulated string slice + emitter.instruction("ret"); // return the accumulated string emit_stream_get_contents_bounded_linux_x86_64(emitter); } @@ -274,8 +361,9 @@ fn emit_stream_get_contents_linux_x86_64(emitter: &mut Emitter) { /// Emits the x86_64 bounded stream_get_contents helper. /// /// Input: `rdi = fd`, `rsi = max bytes`. Output: `rax = ptr`, `rdx = len`. -/// The helper compacts each `__rt_fread` chunk into `_concat_buf` so filters or -/// wrappers that return separate buffers still produce one contiguous result. +/// The helper copies each `__rt_fread` chunk into a reserved accumulation buffer that grows +/// on demand but never past the requested cap, so filters or wrappers that return separate +/// buffers still produce one contiguous result. fn emit_stream_get_contents_bounded_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: stream_get_contents_bounded ---"); @@ -283,14 +371,22 @@ fn emit_stream_get_contents_bounded_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // preserve the caller frame pointer emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer - emitter.instruction("sub rsp, 64"); // reserve aligned locals for bounded accumulation + emitter.instruction("sub rsp, 80"); // reserve aligned locals for bounded accumulation emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the source descriptor emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested byte cap - abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // snapshot the concat-buffer start offset - emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the result start offset - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // materialize the concat-buffer base - emitter.instruction("lea rax, [r11 + r10]"); // compute the result start pointer - emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the result start pointer + + // -- reserve min(cap, initial capacity) and claim the whole window -- + emitter.instruction(&format!("mov rax, {}", INITIAL_CAPACITY)); // start from the initial accumulation capacity + emitter.instruction("cmp rsi, rax"); // is the requested cap smaller than the initial capacity? + emitter.instruction("cmovl rax, rsi"); // never reserve more than the caller asked for + emitter.instruction("xor r8d, r8d"); // a non-positive cap reserves nothing at all + emitter.instruction("cmp rax, 0"); // is the clamped capacity negative? + emitter.instruction("cmovl rax, r8"); // clamp a negative cap to a zero-byte reservation + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the current accumulation capacity + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the accumulated result + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the accumulation buffer pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // publish the full reserved capacity + emitter.instruction("call __rt_concat_publish"); // claim the whole window so each chunk read appends after it emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // running result length = 0 emitter.label("__rt_stream_get_contents_bounded_loop_x86"); @@ -306,15 +402,39 @@ fn emit_stream_get_contents_bounded_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rax, rax"); // did stream_eof report true? emitter.instruction("jnz __rt_stream_get_contents_bounded_done_x86"); // wrapper EOF means no extra stream_read call emitter.label("__rt_stream_get_contents_bounded_after_feof_x86"); + + // -- make room for one more (cap-clamped) chunk before asking fread for it -- + emitter.instruction("mov r8, QWORD PTR [rbp - 40]"); // running result length + emitter.instruction(&format!("add r8, {}", READ_CHUNK)); // capacity needed once the next chunk lands + emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // requested byte cap + emitter.instruction("cmp r8, r9"); // would that exceed the caller's cap? + emitter.instruction("cmovg r8, r9"); // never grow the reservation past the requested cap + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // current accumulation capacity + emitter.instruction("cmp r8, r9"); // does the next chunk still fit the current reservation? + emitter.instruction("jbe __rt_stream_get_contents_bounded_have_room_x86"); // no growth needed for this iteration + emitter.instruction("add r9, r9"); // double the accumulation capacity + emitter.instruction("cmp r9, r8"); // is the doubled capacity already large enough? + emitter.instruction("cmovb r9, r8"); // keep whichever capacity is larger + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // requested byte cap + emitter.instruction("cmp r9, r10"); // did doubling overshoot the caller's cap? + emitter.instruction("cmovg r9, r10"); // clamp the grown capacity to the requested cap + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // save the grown accumulation capacity + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // old accumulation buffer pointer + emitter.instruction("xor edx, edx"); // release the whole claimed window + emitter.instruction("call __rt_concat_publish"); // hand the old scratch window back before moving to heap storage + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // old accumulation buffer pointer + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // bytes accumulated so far must survive the move + emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // grown accumulation capacity + emitter.instruction("call __rt_concat_grow"); // move the accumulated bytes into a larger owned buffer + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the grown accumulation buffer pointer + emitter.label("__rt_stream_get_contents_bounded_have_room_x86"); + emitter.instruction("mov r8, QWORD PTR [rbp - 40]"); // running result length emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // requested byte cap emitter.instruction("sub rsi, r8"); // remaining bytes needed - emitter.instruction("mov r10, 4096"); // maximum chunk request + emitter.instruction(&format!("mov r10, {}", READ_CHUNK)); // maximum chunk request emitter.instruction("cmp rsi, r10"); // is the remaining cap bigger than one chunk? - emitter.instruction("cmovg rsi, r10"); // request min(remaining, 4096) - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // result start offset - emitter.instruction("add r11, r8"); // compact append offset = start + total - abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // make __rt_fread append at the compact tail + emitter.instruction("cmovg rsi, r10"); // request min(remaining, chunk size) emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload fd for __rt_fread emitter.instruction("call __rt_fread"); // rax=chunk ptr, rdx=chunk len emitter.instruction("test rdx, rdx"); // empty chunk? @@ -326,35 +446,39 @@ fn emit_stream_get_contents_bounded_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmova rdx, r9"); // clamp the chunk to the remaining cap emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save chunk pointer across the copy emitter.instruction("mov QWORD PTR [rbp - 56], rdx"); // save chunk length across the copy - abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // materialize the concat-buffer base - emitter.instruction("add r10, QWORD PTR [rbp - 24]"); // result base pointer - emitter.instruction("add r10, r8"); // destination = result base + total + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // accumulation buffer base pointer + emitter.instruction("add r10, r8"); // destination = accumulation base + total emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // source chunk pointer emitter.instruction("xor rcx, rcx"); // byte-copy index emitter.label("__rt_stream_get_contents_bounded_copy_x86"); emitter.instruction("cmp rcx, QWORD PTR [rbp - 56]"); // copied this whole chunk? emitter.instruction("jge __rt_stream_get_contents_bounded_copy_done_x86"); // leave the copy loop once chunk bytes are copied emitter.instruction("mov r9b, BYTE PTR [r11 + rcx]"); // load the next chunk byte - emitter.instruction("mov BYTE PTR [r10 + rcx], r9b"); // store it at the compact destination + emitter.instruction("mov BYTE PTR [r10 + rcx], r9b"); // store it at the accumulation destination emitter.instruction("inc rcx"); // advance the copy index emitter.instruction("jmp __rt_stream_get_contents_bounded_copy_x86"); // copy the next byte emitter.label("__rt_stream_get_contents_bounded_copy_done_x86"); emitter.instruction("mov r8, QWORD PTR [rbp - 40]"); // running result length before this chunk emitter.instruction("add r8, QWORD PTR [rbp - 56]"); // include the copied chunk in the total emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // store the updated result length - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // result start offset - emitter.instruction("add r11, r8"); // compact tail offset after this chunk - abi::emit_store_reg_to_symbol(emitter, "r11", "_concat_off", 0); // publish the compacted concat-buffer tail + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the chunk pointer + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand this chunk's scratch window back for the next read emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the chunk pointer emitter.instruction("call __rt_decref_any"); // release owned wrapper/filter chunks; concat slices are ignored emitter.instruction("jmp __rt_stream_get_contents_bounded_loop_x86"); // read the next bounded chunk emitter.label("__rt_stream_get_contents_bounded_release_done_x86"); + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the final empty chunk pointer + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand the terminal chunk's scratch window back + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // final empty chunk pointer emitter.instruction("call __rt_decref_any"); // release the empty chunk if it is heap-backed emitter.label("__rt_stream_get_contents_bounded_done_x86"); - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the result start pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the accumulation buffer pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return the accumulated result length - emitter.instruction("add rsp, 64"); // release the helper locals + emitter.instruction("call __rt_concat_publish"); // shrink the claimed window down to the bytes actually accumulated + emitter.instruction("add rsp, 80"); // release the helper locals emitter.instruction("pop rbp"); // restore the caller frame pointer - emitter.instruction("ret"); // return the bounded string slice + emitter.instruction("ret"); // return the bounded string } diff --git a/src/codegen_support/runtime/io/stream_get_line.rs b/src/codegen_support/runtime/io/stream_get_line.rs index d9f33eec5a..00f203cb6b 100644 --- a/src/codegen_support/runtime/io/stream_get_line.rs +++ b/src/codegen_support/runtime/io/stream_get_line.rs @@ -6,7 +6,10 @@ //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::io`. //! //! Key details: -//! - Reads one byte at a time into the concat buffer until the byte budget is +//! - The caller's `$length` budget is reserved up front through `__rt_concat_reserve`, so a +//! budget larger than the remaining 64 KiB concat scratch takes owned heap storage instead +//! of writing past `_concat_buf` into the adjacent BSS globals. +//! - Reads one byte at a time into that reservation until the byte budget is //! spent, EOF is reached, or the trailing bytes match the ending delimiter //! (which is consumed and stripped). EOF/read failure sets `_eof_flags`. @@ -16,7 +19,8 @@ use crate::codegen_support::abi; /// stream_get_line: read up to a length or an ending delimiter from a stream. /// Input: x0=fd, x1=max length, x2=ending pointer, x3=ending length -/// Output: x1=string pointer (in concat_buf), x2=length read (delimiter stripped) +/// Output: x1=string pointer (concat scratch or owned heap storage), x2=length read +/// (delimiter stripped) pub fn emit_stream_get_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_stream_get_line_linux_x86_64(emitter); @@ -38,11 +42,11 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.instruction("str x2, [sp, #32]"); // save the ending-delimiter pointer emitter.instruction("str x3, [sp, #40]"); // save the ending-delimiter length - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // current concat-buffer offset - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // result start pointer - emitter.instruction("str x12, [sp, #48]"); // save the result start pointer + emitter.instruction("mov x0, x1"); // the line can never exceed the caller's byte budget + emitter.instruction("cmp x0, #0"); // is the requested budget non-positive? + emitter.instruction("csel x0, xzr, x0, lt"); // a negative budget reserves nothing at all + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the whole budget + emitter.instruction("str x0, [sp, #48]"); // save the result start pointer emitter.instruction("str xzr, [sp, #56]"); // running total starts at zero // -- user-wrapper fd: read via stream_read into _user_wrapper_drain_buf -- @@ -58,10 +62,9 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.instruction("cmp x10, x11"); // reached the byte budget? emitter.instruction("b.ge __rt_stream_get_line_done"); // stop at the maximum length - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // current concat-buffer offset - emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x1, x11, x10"); // single-byte write pointer + emitter.instruction("ldr x1, [sp, #48]"); // reserved result start pointer + emitter.instruction("ldr x10, [sp, #56]"); // running total + emitter.instruction("add x1, x1, x10"); // single-byte write pointer inside the reservation emitter.instruction("ldr x0, [sp, #16]"); // reload the file descriptor emitter.instruction("mov x2, #1"); // read exactly one byte emitter.syscall(3); @@ -79,10 +82,6 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.label("__rt_stream_get_line_read_ok"); emitter.instruction("cbz x0, __rt_stream_get_line_eof"); // a zero-byte read means EOF - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // concat-buffer offset - emitter.instruction("add x10, x10, #1"); // advance past the byte just read - emitter.instruction("str x10, [x9]"); // publish the updated offset emitter.instruction("ldr x10, [sp, #56]"); // running total emitter.instruction("add x10, x10, #1"); // count the new byte emitter.instruction("str x10, [sp, #56]"); // store the running total @@ -112,10 +111,6 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.instruction("ldr x10, [sp, #56]"); // running total emitter.instruction("sub x10, x10, x3"); // drop the delimiter from the result emitter.instruction("str x10, [sp, #56]"); // store the stripped total - emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // concat-buffer offset - emitter.instruction("sub x10, x10, x3"); // rewind past the consumed delimiter - emitter.instruction("str x10, [x9]"); // publish the rewound offset emitter.instruction("b __rt_stream_get_line_done"); // a delimiter match is not EOF // -- user-wrapper line read: feof-gated stream_read into _user_wrapper_drain_buf @@ -143,6 +138,8 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.instruction("strb w13, [x12, x10]"); // append the byte to the line buffer emitter.instruction("add x10, x10, #1"); // advance the running total emitter.instruction("str x10, [sp, #56]"); // store the updated total + emitter.instruction("mov x2, #0"); // release the whole chunk window + emitter.instruction("bl __rt_concat_publish"); // hand this chunk's scratch window back before the next read emitter.instruction("mov x0, x1"); // chunk ptr (byte already copied) emitter.instruction("bl __rt_decref_any"); // release the owned chunk emitter.instruction("ldr x3, [sp, #40]"); // ending-delimiter length @@ -179,6 +176,7 @@ pub fn emit_stream_get_line(emitter: &mut Emitter) { emitter.label("__rt_stream_get_line_done"); emitter.instruction("ldr x1, [sp, #48]"); // return the result start pointer emitter.instruction("ldr x2, [sp, #56]"); // return the bytes read + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // release the frame emitter.instruction("ret"); // return the line slice @@ -200,10 +198,12 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the ending-delimiter pointer emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the ending-delimiter length - abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // current concat-buffer offset - abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // concat-buffer base address - emitter.instruction("lea r11, [r10 + r9]"); // result start pointer - emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the result start pointer + emitter.instruction("mov rax, rsi"); // the line can never exceed the caller's byte budget + emitter.instruction("xor r8d, r8d"); // a negative budget reserves nothing at all + emitter.instruction("cmp rax, 0"); // is the requested budget non-positive? + emitter.instruction("cmovl rax, r8"); // clamp a negative budget to a zero-byte reservation + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the whole budget + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the result start pointer emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // running total starts at zero // -- user-wrapper fd: read via stream_read into _user_wrapper_drain_buf -- @@ -217,9 +217,8 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp rax, QWORD PTR [rbp - 16]"); // reached the byte budget? emitter.instruction("jge __rt_stream_get_line_done_x86"); // stop at the maximum length - abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // current concat-buffer offset - abi::emit_symbol_address(emitter, "r10", "_concat_buf"); // concat-buffer base address - emitter.instruction("lea rsi, [r10 + r9]"); // single-byte write pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reserved result start pointer + emitter.instruction("add rsi, QWORD PTR [rbp - 48]"); // single-byte write pointer inside the reservation emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the file descriptor emitter.instruction("mov rdx, 1"); // read exactly one byte emitter.instruction("call read"); // read one byte through libc read() @@ -229,9 +228,6 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_stream_get_line_eof_x86"); // zero-byte read means real EOF emitter.label("__rt_stream_get_line_read_ok_x86"); - abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // concat-buffer offset - emitter.instruction("inc r9"); // advance past the byte just read - abi::emit_store_reg_to_symbol(emitter, "r9", "_concat_off", 0); // publish the updated offset emitter.instruction("inc QWORD PTR [rbp - 48]"); // count the new byte emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // ending-delimiter length @@ -260,9 +256,6 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // running total emitter.instruction("sub rax, rcx"); // drop the delimiter from the result emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // store the stripped total - abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // concat-buffer offset - emitter.instruction("sub r9, rcx"); // rewind past the consumed delimiter - abi::emit_store_reg_to_symbol(emitter, "r9", "_concat_off", 0); // publish the rewound offset emitter.instruction("jmp __rt_stream_get_line_done_x86"); // a delimiter match is not EOF emitter.label("__rt_stream_get_line_read_failed_x86"); @@ -296,6 +289,8 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov BYTE PTR [r11 + r10], cl"); // append the byte to the line buffer emitter.instruction("inc r10"); // advance the running total emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // store the updated total + emitter.instruction("xor edx, edx"); // release the whole chunk window + emitter.instruction("call __rt_concat_publish"); // hand this chunk's scratch window back before the next read emitter.instruction("call __rt_decref_any"); // release the owned chunk (rax = chunk ptr) emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // ending-delimiter length emitter.instruction("test rcx, rcx"); // no delimiter configured? @@ -332,6 +327,7 @@ fn emit_stream_get_line_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_stream_get_line_done_x86"); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the result start pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // return the bytes read + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 48"); // release the frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the line slice diff --git a/src/codegen_support/runtime/io/var_dump_walk.rs b/src/codegen_support/runtime/io/var_dump_walk.rs index 44d400250b..bd4973e3af 100644 --- a/src/codegen_support/runtime/io/var_dump_walk.rs +++ b/src/codegen_support/runtime/io/var_dump_walk.rs @@ -599,7 +599,7 @@ pub fn emit_var_dump_emit_float_line(emitter: &mut Emitter) { emitter.instruction("bl __rt_vd_write"); // write x1/x2 through the ob/web-aware stdout sink (register-preserving) // ftoa(d0) → x1=ptr, x2=len - emitter.instruction("bl __rt_ftoa"); // call runtime helper + emitter.instruction("bl __rt_ftoa_repr"); // render at serialize_precision=-1 (var_dump layout) emitter.instruction("bl __rt_vd_write"); // write x1/x2 through the ob/web-aware stdout sink (register-preserving) // Emit ")\n" @@ -629,7 +629,7 @@ fn emit_var_dump_emit_float_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call __rt_vd_write"); // write rsi/rdx through the ob/web-aware stdout sink (register-preserving) emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload xmm0 for ftoa - emitter.instruction("call __rt_ftoa"); // rax=ptr, rdx=len + emitter.instruction("call __rt_ftoa_repr"); // serialize_precision=-1 layout: rax=ptr, rdx=len emitter.instruction("mov rsi, rax"); // prepare SysV call argument emitter.instruction("call __rt_vd_write"); // write rsi/rdx through the ob/web-aware stdout sink (register-preserving) @@ -1064,6 +1064,16 @@ pub fn emit_var_dump_value(emitter: &mut Emitter) { emitter.label("__rt_vd_val_obj"); emitter.instruction("ldr x0, [sp, #0]"); // nested object pointer emitter.instruction("cbz x0, __rt_vd_val_null"); // defensive: a null instance renders NULL + // PHP renders an enum case as `enum(E::C)`, never as an object body, so the + // enum test happens before anything object-shaped is written. + emitter.instruction("bl __rt_obj_enum_name_offset"); // x0 = enum `name` slot offset, or -1 for a plain class + emitter.instruction("cmp x0, #0"); // is this instance an enum case? + emitter.instruction("b.lt __rt_vd_val_obj_plain"); // a plain class falls through to the object body + emitter.instruction("ldr x0, [sp, #0]"); // reload the enum instance pointer + emitter.instruction("bl __rt_var_dump_emit_enum_line"); // emit `enum(E::C)\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + emitter.label("__rt_vd_val_obj_plain"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer emitter.instruction("bl __rt_vd_seen_find"); // is this object already on the walk stack? emitter.instruction("cbnz x0, __rt_vd_val_recursion"); // PHP renders a revisited object as *RECURSION* emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer @@ -1179,6 +1189,16 @@ fn emit_var_dump_value_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // nested object pointer emitter.instruction("test rdi, rdi"); // defensive null-instance check emitter.instruction("jz __rt_vd_val_null_x86"); // a null instance renders NULL + // PHP renders an enum case as `enum(E::C)`, never as an object body, so the + // enum test happens before anything object-shaped is written. + emitter.instruction("call __rt_obj_enum_name_offset"); // rax = enum `name` slot offset, or -1 for a plain class + emitter.instruction("cmp rax, 0"); // is this instance an enum case? + emitter.instruction("jl __rt_vd_val_obj_plain_x86"); // a plain class falls through to the object body + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the enum instance pointer + emitter.instruction("call __rt_var_dump_emit_enum_line"); // emit `enum(E::C)\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + emitter.label("__rt_vd_val_obj_plain_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the object pointer emitter.instruction("call __rt_vd_seen_find"); // is this object already on the walk stack? emitter.instruction("test rax, rax"); // did the guard report a revisit? emitter.instruction("jnz __rt_vd_val_recursion_x86"); // PHP renders a revisited object as *RECURSION* diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index 3a96f68298..01bc4941c2 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -12,7 +12,9 @@ mod arrays; mod buffers; mod callables; -mod data; +/// PHP loose-equality (`==`) walkers for boxed Mixed values, arrays, and objects. +mod compare; +pub(crate) mod data; mod diagnostics; mod emitters; mod eval_bridge; @@ -22,11 +24,15 @@ mod fibers; /// Runtime helpers for generator state management (yield, resume, stack frames). pub(crate) mod generators; mod io; +/// The shared PHP `float`→`int` conversion (`__rt_php_float_to_int`). +mod numeric; mod objects; /// PDO Tier-D callback adapters (`__rt_pdo_*`) re-entering compiled-PHP callables. mod pdo; mod pointers; mod resource_ids; +/// PHP's `round($num, $precision, $mode)` runtime implementation (`__rt_round_mode`). +mod round_mode; /// Standard PHP library constants, functions, and classes. pub(crate) mod spl; mod strings; @@ -62,6 +68,9 @@ pub(crate) use emitters::emit_runtime; pub(crate) use arrays::{emit_nan_bool_coercion_probe, nan_bool_coercion_warning_enabled}; /// The `__rt_hash_map` callback result-kind selector, chosen by the `array_map()` lowering. pub(crate) use arrays::HashMapResultKind; +/// The call-stack overflow guard's shared symbol name. Codegen's prologue check and the +/// runtime emitter must name the same `.comm` word or the guard silently never fires. +pub(crate) use system::STACK_LIMIT_SYMBOL; /// Emit full runtime helpers (orchestrates all runtime sections). pub(crate) use fibers::{ FIBER_CALLABLE_OFFSET, FIBER_PENDING_THROW_OFFSET, FIBER_STACK_BASE_OFFSET, diff --git a/src/codegen_support/runtime/numeric.rs b/src/codegen_support/runtime/numeric.rs new file mode 100644 index 0000000000..ee09445685 --- /dev/null +++ b/src/codegen_support/runtime/numeric.rs @@ -0,0 +1,133 @@ +//! Purpose: +//! Emits `__rt_php_float_to_int`, the single shared PHP `float`→`int` conversion used by +//! every cast, array-key, and numeric-coercion site on every supported target. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()`. +//! - Indirectly from every `crate::codegen_support::abi::emit_php_float_to_int()` call site. +//! +//! Key details: +//! - Reference PHP 8.4 (`zend_dval_to_lval`) casts NaN and ±INF to `0` and reduces any other +//! out-of-range finite double modulo 2^64 before reinterpreting it as a signed 64-bit value. +//! Raw hardware truncation does neither: AArch64 `fcvtzs` saturates to `INT64_MIN`/`INT64_MAX` +//! while x86_64 `cvttsd2si` yields `INT64_MIN` for every invalid input, so the two supported +//! architectures used to disagree with PHP *and* with each other. +//! - The helper therefore decodes the IEEE-754 fields with integer instructions only. That is +//! exact by construction and produces bit-identical results on AArch64 and x86_64. +//! - ABI: the double arrives in the float result register (`d0` / `xmm0`); the converted integer +//! is returned in the *symbol scratch* register (`x9` / `r11`), not in the int result register. +//! Every other register — including `x0`/`rax` and the whole floating-point file — is +//! preserved, so the helper can be called from lowering sites that still hold live values. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_php_float_to_int` runtime helper for the active target. +/// +/// # Input +/// - AArch64: the source double in `d0`; x86_64: the source double in `xmm0`. +/// +/// # Output +/// - AArch64: the PHP integer value in `x9`; x86_64: the PHP integer value in `r11`. +/// +/// # Clobbers +/// - Only the output register (plus `x30` on AArch64, as for any `bl`). Callers may keep live +/// values in every other integer and floating-point register across the call. +pub fn emit_php_float_to_int(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_php_float_to_int_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: php_float_to_int ---"); + emitter.label_global("__rt_php_float_to_int"); + + // -- decode the IEEE-754 fields of the incoming double -- + emitter.instruction("stp x10, x11, [sp, #-16]!"); // preserve the two scratch registers this leaf helper needs + emitter.instruction("fmov x9, d0"); // raw IEEE-754 bit pattern of the source double + emitter.instruction("ubfx x10, x9, #52, #11"); // biased exponent field + emitter.instruction("cmp x10, #1023"); // is the magnitude below 1.0? + emitter.instruction("b.lo __rt_php_float_to_int_zero"); // PHP truncates |d| < 1 (and zero/subnormals) to 0 + emitter.instruction("and x11, x9, #0x000fffffffffffff"); // 52-bit fraction field + emitter.instruction("orr x11, x11, #0x0010000000000000"); // restore the implicit leading significand bit + emitter.instruction("sub x10, x10, #1075"); // binary shift = exponent - bias - mantissa width + emitter.instruction("cmp x10, #64"); // would every significand bit leave the 64-bit window? + emitter.instruction("b.ge __rt_php_float_to_int_zero"); // yes: PHP's modulo-2^64 reduction is 0 (also covers NaN/±INF) + + // -- shift the significand into place, wrapping modulo 2^64 exactly like PHP -- + emitter.instruction("tbnz x10, #63, __rt_php_float_to_int_right"); // a negative shift means the value has a fractional part + emitter.instruction("lsl x11, x11, x10"); // scale the significand up, keeping only the low 64 bits + emitter.instruction("b __rt_php_float_to_int_sign"); // apply the sign to the computed magnitude + + emitter.label("__rt_php_float_to_int_right"); + emitter.instruction("neg x10, x10"); // turn the negative shift into a right-shift distance + emitter.instruction("lsr x11, x11, x10"); // drop the fractional bits, truncating toward zero + + emitter.label("__rt_php_float_to_int_sign"); + emitter.instruction("tbnz x9, #63, __rt_php_float_to_int_negate"); // negative doubles need a two's complement result + emitter.instruction("mov x9, x11"); // non-negative doubles return the magnitude unchanged + emitter.instruction("b __rt_php_float_to_int_done"); // fall through to the shared epilogue + + emitter.label("__rt_php_float_to_int_negate"); + emitter.instruction("neg x9, x11"); // negate modulo 2^64 for negative doubles + emitter.instruction("b __rt_php_float_to_int_done"); // fall through to the shared epilogue + + emitter.label("__rt_php_float_to_int_zero"); + emitter.instruction("mov x9, #0"); // PHP casts NaN, ±INF and fully-out-of-window values to 0 + + emitter.label("__rt_php_float_to_int_done"); + emitter.instruction("ldp x10, x11, [sp], #16"); // restore the preserved scratch registers + emitter.instruction("ret"); // return the PHP integer value in x9 +} + +/// Emits the x86_64 variant of `__rt_php_float_to_int`. +/// +/// Mirrors the AArch64 decode exactly: `r11` holds the result, `r10` the raw bit pattern and +/// `rcx` the shift count. Both scratch registers are pushed so the helper only clobbers `r11`. +fn emit_php_float_to_int_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_float_to_int ---"); + emitter.label_global("__rt_php_float_to_int"); + + // -- decode the IEEE-754 fields of the incoming double -- + emitter.instruction("push rcx"); // preserve the shift-count register this leaf helper needs + emitter.instruction("push r10"); // preserve the raw-bit-pattern scratch register + emitter.instruction("movq r10, xmm0"); // raw IEEE-754 bit pattern of the source double + emitter.instruction("mov rcx, r10"); // copy the bit pattern before extracting the exponent + emitter.instruction("shr rcx, 52"); // move the exponent field into the low bits + emitter.instruction("and ecx, 0x7ff"); // biased exponent field + emitter.instruction("cmp rcx, 1023"); // is the magnitude below 1.0? + emitter.instruction("jb __rt_php_float_to_int_zero_x86_64"); // PHP truncates |d| < 1 (and zero/subnormals) to 0 + emitter.instruction("mov r11, r10"); // copy the bit pattern to build the significand + emitter.instruction("shl r11, 12"); // drop the sign and exponent fields + emitter.instruction("shr r11, 12"); // keep only the 52-bit fraction field + emitter.instruction("bts r11, 52"); // restore the implicit leading significand bit + emitter.instruction("sub rcx, 1075"); // binary shift = exponent - bias - mantissa width + emitter.instruction("cmp rcx, 64"); // would every significand bit leave the 64-bit window? + emitter.instruction("jge __rt_php_float_to_int_zero_x86_64"); // yes: PHP's modulo-2^64 reduction is 0 (also covers NaN/±INF) + + // -- shift the significand into place, wrapping modulo 2^64 exactly like PHP -- + emitter.instruction("test rcx, rcx"); // is the shift negative? + emitter.instruction("js __rt_php_float_to_int_right_x86_64"); // a negative shift means the value has a fractional part + emitter.instruction("shl r11, cl"); // scale the significand up, keeping only the low 64 bits + emitter.instruction("jmp __rt_php_float_to_int_sign_x86_64"); // apply the sign to the computed magnitude + + emitter.label("__rt_php_float_to_int_right_x86_64"); + emitter.instruction("neg rcx"); // turn the negative shift into a right-shift distance + emitter.instruction("shr r11, cl"); // drop the fractional bits, truncating toward zero + + emitter.label("__rt_php_float_to_int_sign_x86_64"); + emitter.instruction("test r10, r10"); // was the source double negative? + emitter.instruction("jns __rt_php_float_to_int_done_x86_64"); // non-negative doubles return the magnitude unchanged + emitter.instruction("neg r11"); // negate modulo 2^64 for negative doubles + emitter.instruction("jmp __rt_php_float_to_int_done_x86_64"); // fall through to the shared epilogue + + emitter.label("__rt_php_float_to_int_zero_x86_64"); + emitter.instruction("xor r11d, r11d"); // PHP casts NaN, ±INF and fully-out-of-window values to 0 + + emitter.label("__rt_php_float_to_int_done_x86_64"); + emitter.instruction("pop r10"); // restore the raw-bit-pattern scratch register + emitter.instruction("pop rcx"); // restore the shift-count register + emitter.instruction("ret"); // return the PHP integer value in r11 +} diff --git a/src/codegen_support/runtime/objects/enum_debug.rs b/src/codegen_support/runtime/objects/enum_debug.rs new file mode 100644 index 0000000000..34506aec5e --- /dev/null +++ b/src/codegen_support/runtime/objects/enum_debug.rs @@ -0,0 +1,282 @@ +//! Purpose: +//! Emits the runtime helpers that let the value renderers recognize a PHP enum +//! case behind an ordinary object pointer: `__rt_obj_enum_kind`, +//! `__rt_obj_enum_name_offset`, `__rt_obj_enum_case_name`, and the +//! `__rt_var_dump_emit_enum_line` line emitter that renders `enum(E::C)`. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::objects`. +//! - `__rt_vd_val_obj` (tag 6) in `runtime::io::var_dump_walk`, which consults +//! `__rt_obj_enum_name_offset` before opening an object body. +//! - `__rt_print_r_object` in `super::print_r_object`, for PHP's ` Enum` / +//! ` Enum:int` / ` Enum:string` header suffix. +//! - The `__elephc_object_is_enum` prelude builtin used by `var_export`. +//! +//! Key details: +//! - Enum-ness is a property of the CLASS, not of the instance: the object header +//! carries a class id, and `_class_enum_kinds[class_id]` / +//! `_class_enum_name_offsets[class_id]` (emitted by +//! `crate::codegen_support::runtime::data::user`) answer both questions with one +//! indexed load. Kind is `0` for a plain class and `1`/`2`/`3` for a pure / +//! int-backed / string-backed enum; the name offset is `-1` for a plain class. +//! - Both tables are bounds-checked against `_class_gc_desc_count`, the shared +//! class-id table extent every other per-class lookup uses, so a synthetic or +//! stale class id reports "not an enum" instead of reading past the table. +//! - elephc materializes PHP's readonly `name` case property as an ordinary +//! declared string property, so the case name is just the 16-byte `(ptr, len)` +//! pair at that offset — no case table lookup and no allocation. +//! - `__rt_var_dump_emit_enum_line` writes through `__rt_vd_pad` / `__rt_vd_write`, +//! the same indent-aware sink every other var_dump line uses, so a nested enum +//! inside an array or object lands at the right column for free. + +use crate::codegen_support::abi; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// `__rt_obj_enum_kind`: classify an object's class as plain or enum. +/// +/// Input: AArch64 x0 / x86_64 rdi = object pointer. +/// Output: AArch64 x0 / x86_64 rax = 0 for a plain class, 1 for a pure enum, +/// 2 for an int-backed enum, 3 for a string-backed enum. +pub fn emit_obj_enum_kind(emitter: &mut Emitter) { + emit_class_table_lookup( + emitter, + "__rt_obj_enum_kind", + "_class_enum_kinds", + "__rt_obj_enum_kind_none", + 0, + ); +} + +/// `__rt_obj_enum_name_offset`: locate an enum instance's `name` property slot. +/// +/// Input: AArch64 x0 / x86_64 rdi = object pointer. +/// Output: AArch64 x0 / x86_64 rax = byte offset of the `name` slot within the +/// instance, or `-1` when the class is not an enum (which is also what every +/// caller uses as the "this is an ordinary object" test). +pub fn emit_obj_enum_name_offset(emitter: &mut Emitter) { + emit_class_table_lookup( + emitter, + "__rt_obj_enum_name_offset", + "_class_enum_name_offsets", + "__rt_obj_enum_name_offset_none", + -1, + ); +} + +/// Emits a bounds-checked `table[object->class_id]` lookup helper. +/// +/// `miss_label` names the out-of-range arm and `miss_value` is what an unknown +/// class id reports; both tables this serves are `.quad`-per-class-id and are +/// sized from the same `_class_gc_desc_count` extent as the descriptor tables. +fn emit_class_table_lookup( + emitter: &mut Emitter, + symbol: &str, + table: &str, + miss_label: &str, + miss_value: i64, +) { + emitter.blank(); + emitter.comment(&format!("--- runtime: {} ---", symbol.trim_start_matches("__rt_"))); + emitter.label_global(symbol); + + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("ldr x10, [x10]"); // load the number of registered class ids + emitter.instruction("cmp x9, x10"); // is the class id within the per-class tables? + emitter.instruction(&format!("b.hs {}", miss_label)); // an unknown class id reports the miss value + abi::emit_symbol_address(emitter, "x11", table); // resolve the per-class enum table + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // load this class's entry + emitter.instruction("ret"); // return to caller + emitter.label(miss_label); + emitter.instruction(&format!("mov x0, #{}", miss_value)); // report the not-an-enum value + emitter.instruction("ret"); // return to caller + } + Arch::X86_64 => { + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of registered class ids + emitter.instruction("cmp r9, r10"); // is the class id within the per-class tables? + emitter.instruction(&format!("jae {}_x86", miss_label)); // an unknown class id reports the miss value + abi::emit_symbol_address(emitter, "r11", table); // resolve the per-class enum table + emitter.instruction("mov rax, QWORD PTR [r11 + r9 * 8]"); // load this class's entry + emitter.instruction("ret"); // return to caller + emitter.label(&format!("{}_x86", miss_label)); + emitter.instruction(&format!("mov rax, {}", miss_value)); // report the not-an-enum value + emitter.instruction("ret"); // return to caller + } + } +} + +/// `__rt_obj_enum_case_name`: read an enum instance's case-name string. +/// +/// Input: AArch64 x0 / x86_64 rdi = object pointer of a class already known to be +/// an enum. Output: AArch64 x0=ptr x1=len / x86_64 rax=ptr rdx=len. A class with +/// no `name` slot yields a zero-length string rather than a wild pointer. +pub fn emit_obj_enum_case_name(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_enum_case_name ---"); + emitter.label_global("__rt_obj_enum_case_name"); + + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("sub sp, sp, #32"); // allocate the case-name frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish the case-name frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the object pointer across the lookup + emitter.instruction("bl __rt_obj_enum_name_offset"); // x0 = `name` slot byte offset, or -1 + emitter.instruction("cmp x0, #0"); // does this class carry a `name` slot? + emitter.instruction("b.lt __rt_obj_enum_case_name_none"); // a plain class reports an empty case name + emitter.instruction("ldr x9, [sp, #0]"); // reload the object pointer + emitter.instruction("add x9, x9, x0"); // resolve the absolute `name` slot address + emitter.instruction("ldr x1, [x9, #8]"); // load the case-name length from the slot high word + emitter.instruction("ldr x0, [x9]"); // load the case-name pointer from the slot low word + emitter.instruction("b __rt_obj_enum_case_name_done"); // return the resolved case name + emitter.label("__rt_obj_enum_case_name_none"); + emitter.instruction("mov x0, #0"); // no `name` slot → null pointer + emitter.instruction("mov x1, #0"); // no `name` slot → zero length + emitter.label("__rt_obj_enum_case_name_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the case-name frame + emitter.instruction("ret"); // return to caller + } + Arch::X86_64 => { + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the case-name frame pointer + emitter.instruction("sub rsp, 16"); // allocate the case-name frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the object pointer across the lookup + emitter.instruction("call __rt_obj_enum_name_offset"); // rax = `name` slot byte offset, or -1 + emitter.instruction("cmp rax, 0"); // does this class carry a `name` slot? + emitter.instruction("jl __rt_obj_enum_case_name_none_x86"); // a plain class reports an empty case name + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the object pointer + emitter.instruction("add r9, rax"); // resolve the absolute `name` slot address + emitter.instruction("mov rdx, QWORD PTR [r9 + 8]"); // load the case-name length from the slot high word + emitter.instruction("mov rax, QWORD PTR [r9]"); // load the case-name pointer from the slot low word + emitter.instruction("jmp __rt_obj_enum_case_name_done_x86"); // return the resolved case name + emitter.label("__rt_obj_enum_case_name_none_x86"); + emitter.instruction("xor rax, rax"); // no `name` slot → null pointer + emitter.instruction("xor rdx, rdx"); // no `name` slot → zero length + emitter.label("__rt_obj_enum_case_name_done_x86"); + emitter.instruction("add rsp, 16"); // release the case-name frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller + } + } +} + +/// `__rt_var_dump_emit_enum_line`: write `enum(Class::Case)\n`. +/// +/// This is what PHP prints for an enum case instead of an object body, at every +/// nesting depth. Input: AArch64 x0 / x86_64 rdi = enum instance pointer. +pub fn emit_var_dump_emit_enum_line(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_var_dump_emit_enum_line_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: var_dump_emit_enum_line ---"); + emitter.label_global("__rt_var_dump_emit_enum_line"); + + // Frame (32 bytes): [0] object ptr, [16] saved x29, [24] saved x30. + emitter.instruction("sub sp, sp, #32"); // allocate the enum-line frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish the enum-line frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the enum instance pointer + + emitter.instruction("bl __rt_vd_pad"); // indent the enum line to the current depth + abi::emit_symbol_address(emitter, "x1", "_vd_enum_prefix"); // load the `enum(` prefix + emitter.instruction("mov x2, #5"); // len("enum(") = 5 + emitter.instruction("bl __rt_vd_write"); // write `enum(` + + // -- class name from the shared class-id → (name ptr, name len) table -- + emitter.instruction("ldr x9, [sp, #0]"); // reload the enum instance pointer + emitter.instruction("ldr x9, [x9]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_name_count"); // resolve the class-name table extent + emitter.instruction("ldr x10, [x10]"); // load the number of named class ids + emitter.instruction("cmp x9, x10"); // is the class id within the name table? + emitter.instruction("b.hs __rt_vd_enum_anon"); // an unknown class id writes no name + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); // resolve the class-name entry table + emitter.instruction("add x11, x11, x9, lsl #4"); // each entry is a 16-byte (ptr, len) pair + emitter.instruction("ldr x1, [x11]"); // load the class-name pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the class-name length + emitter.instruction("b __rt_vd_enum_name"); // write the resolved name + emitter.label("__rt_vd_enum_anon"); + abi::emit_symbol_address(emitter, "x1", "_class_name_missing"); // fall back to the empty class-name slot + emitter.instruction("mov x2, #0"); // a zero-length write emits nothing + emitter.label("__rt_vd_enum_name"); + emitter.instruction("bl __rt_vd_write"); // write the enum class name + + abi::emit_symbol_address(emitter, "x1", "_vd_enum_sep"); // load the `::` case separator + emitter.instruction("mov x2, #2"); // len("::") = 2 + emitter.instruction("bl __rt_vd_write"); // write `::` + + emitter.instruction("ldr x0, [sp, #0]"); // reload the enum instance pointer + emitter.instruction("bl __rt_obj_enum_case_name"); // x0=case-name ptr, x1=case-name len + emitter.instruction("mov x2, x1"); // case-name length → write length argument + emitter.instruction("mov x1, x0"); // case-name pointer → write buffer argument + emitter.instruction("bl __rt_vd_write"); // write the case name + + abi::emit_symbol_address(emitter, "x1", "_vd_enum_close"); // load the `)\n` terminator + emitter.instruction("mov x2, #2"); // len(")\n") = 2 + emitter.instruction("bl __rt_vd_write"); // write `)\n` + + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the enum-line frame + emitter.instruction("ret"); // return to caller +} + +/// Emits the Linux x86_64 `enum(Class::Case)` var_dump line emitter. +fn emit_var_dump_emit_enum_line_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: var_dump_emit_enum_line ---"); + emitter.label_global("__rt_var_dump_emit_enum_line"); + + // rbp-relative frame: [-8] object ptr. + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the enum-line frame pointer + emitter.instruction("sub rsp, 16"); // allocate the enum-line frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the enum instance pointer + + emitter.instruction("call __rt_vd_pad"); // indent the enum line to the current depth + abi::emit_symbol_address(emitter, "rsi", "_vd_enum_prefix"); // load the `enum(` prefix + emitter.instruction("mov edx, 5"); // len("enum(") = 5 + emitter.instruction("call __rt_vd_write"); // write `enum(` + + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the enum instance pointer + emitter.instruction("mov r9, QWORD PTR [r9]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_name_count"); // resolve the class-name table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of named class ids + emitter.instruction("cmp r9, r10"); // is the class id within the name table? + emitter.instruction("jae __rt_vd_enum_anon_x86"); // an unknown class id writes no name + abi::emit_symbol_address(emitter, "r11", "_class_name_entries"); // resolve the class-name entry table + emitter.instruction("shl r9, 4"); // each entry is a 16-byte (ptr, len) pair + emitter.instruction("add r11, r9"); // advance to this class's entry + emitter.instruction("mov rsi, QWORD PTR [r11]"); // load the class-name pointer + emitter.instruction("mov rdx, QWORD PTR [r11 + 8]"); // load the class-name length + emitter.instruction("jmp __rt_vd_enum_name_x86"); // write the resolved name + emitter.label("__rt_vd_enum_anon_x86"); + abi::emit_symbol_address(emitter, "rsi", "_class_name_missing"); // fall back to the empty class-name slot + emitter.instruction("xor edx, edx"); // a zero-length write emits nothing + emitter.label("__rt_vd_enum_name_x86"); + emitter.instruction("call __rt_vd_write"); // write the enum class name + + abi::emit_symbol_address(emitter, "rsi", "_vd_enum_sep"); // load the `::` case separator + emitter.instruction("mov edx, 2"); // len("::") = 2 + emitter.instruction("call __rt_vd_write"); // write `::` + + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the enum instance pointer + emitter.instruction("call __rt_obj_enum_case_name"); // rax=case-name ptr, rdx=case-name len + emitter.instruction("mov rsi, rax"); // case-name pointer → write buffer argument + emitter.instruction("call __rt_vd_write"); // write the case name + + abi::emit_symbol_address(emitter, "rsi", "_vd_enum_close"); // load the `)\n` terminator + emitter.instruction("mov edx, 2"); // len(")\n") = 2 + emitter.instruction("call __rt_vd_write"); // write `)\n` + + emitter.instruction("add rsp, 16"); // release the enum-line frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} diff --git a/src/codegen_support/runtime/objects/export_props.rs b/src/codegen_support/runtime/objects/export_props.rs new file mode 100644 index 0000000000..7ef097341e --- /dev/null +++ b/src/codegen_support/runtime/objects/export_props.rs @@ -0,0 +1,306 @@ +//! Purpose: +//! Emits the `__rt_obj_prop_count` / `__rt_obj_prop_name` / `__rt_obj_prop_value` +//! runtime helpers: indexed, PHP-callable access to an object's renderable +//! properties, which is what lets the injected `var_export` prelude walk an object +//! in ordinary PHP instead of in assembly. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::objects`. +//! - The `__elephc_object_prop_*` builtins lowered by +//! `crate::codegen::lower_inst::builtins::object_props`. +//! +//! Key details: +//! - All three read `_class_prop_desc_ptrs[class_id]`, the SAME descriptor +//! `__rt_print_r_object` walks and the same rows `_class_vd_desc_*` carries, so +//! `var_export`, `print_r` and `var_dump` cannot disagree about an object's +//! shape. Row layout is 48 bytes: +//! `(print_r_key_ptr, print_r_key_len, byte_offset, value_tag, name_ptr, name_len)`. +//! - A null instance, an out-of-range index, and an uninitialized typed property +//! are all "no property here": the count is 0, the name is the empty string, and +//! the value is boxed PHP null. PHP omits uninitialized typed properties from +//! `var_export` output, and no real property name is empty, so the prelude skips +//! on an empty name without needing a fourth accessor. +//! - `__rt_obj_prop_value` ALWAYS returns a freshly boxed cell: a slot that already +//! holds a `Mixed` cell is unboxed and re-boxed rather than handed back, because +//! the caller owns and releases what it receives and must not be able to free +//! storage that belongs to the object. `__rt_mixed_from_value` persists strings +//! and increfs containers/objects, so the copy is independently owned. +//! - The two leaf helpers make no calls and touch caller-saved scratch only; +//! `__rt_obj_prop_value` sets up a frame because it calls into the boxing helpers. + +use crate::codegen_support::abi; +use crate::codegen_support::sentinels::{emit_branch_if_null_container, NULL_SENTINEL}; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Byte width of one `_class_prop_desc_*` property row. +const PROP_DESC_ROW_BYTES: u64 = 48; + +/// `__rt_obj_prop_count`: number of properties an object renders. +/// +/// Input: AArch64 x0 / x86_64 rdi = object pointer (0 for a non-object). +/// Output: AArch64 x0 / x86_64 rax = row count, 0 when there is no descriptor. +pub fn emit_obj_prop_count(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_prop_count ---"); + emitter.label_global("__rt_obj_prop_count"); + + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("cbz x0, __rt_obj_prop_count_none"); // a non-object value has no properties + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("ldr x10, [x10]"); // load the number of registered class ids + emitter.instruction("cmp x9, x10"); // is the class id within the descriptor table? + emitter.instruction("b.hs __rt_obj_prop_count_none"); // an unknown class renders no properties + abi::emit_symbol_address(emitter, "x11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("ldr x11, [x11, x9, lsl #3]"); // load this class's property descriptor + emitter.instruction("ldr x0, [x11]"); // the row count sits at descriptor offset 0 + emitter.instruction("ret"); // return to caller + emitter.label("__rt_obj_prop_count_none"); + emitter.instruction("mov x0, #0"); // report zero renderable properties + emitter.instruction("ret"); // return to caller + } + Arch::X86_64 => { + emitter.instruction("test rdi, rdi"); // a non-object value has no properties + emitter.instruction("jz __rt_obj_prop_count_none_x86"); // report zero renderable properties + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of registered class ids + emitter.instruction("cmp r9, r10"); // is the class id within the descriptor table? + emitter.instruction("jae __rt_obj_prop_count_none_x86"); // an unknown class renders no properties + abi::emit_symbol_address(emitter, "r11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("mov r11, QWORD PTR [r11 + r9 * 8]"); // load this class's property descriptor + emitter.instruction("mov rax, QWORD PTR [r11]"); // the row count sits at descriptor offset 0 + emitter.instruction("ret"); // return to caller + emitter.label("__rt_obj_prop_count_none_x86"); + emitter.instruction("xor rax, rax"); // report zero renderable properties + emitter.instruction("ret"); // return to caller + } + } +} + +/// `__rt_obj_prop_name`: bare name of an object's Nth renderable property. +/// +/// Input: AArch64 x0=object x1=index / x86_64 rdi=object rsi=index. +/// Output: the platform string result pair (AArch64 x1=ptr x2=len, x86_64 +/// rax=ptr rdx=len). An absent, out-of-range or still-uninitialized property +/// yields a zero-length string, which is what the prelude skips on. +pub fn emit_obj_prop_name(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_prop_name ---"); + emitter.label_global("__rt_obj_prop_name"); + + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("cbz x0, __rt_obj_prop_name_none"); // a non-object value has no property names + emitter.instruction("cmp x1, #0"); // reject a negative index before scaling it + emitter.instruction("b.lt __rt_obj_prop_name_none"); // out-of-range indices yield the empty string + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("ldr x10, [x10]"); // load the number of registered class ids + emitter.instruction("cmp x9, x10"); // is the class id within the descriptor table? + emitter.instruction("b.hs __rt_obj_prop_name_none"); // an unknown class has no property names + abi::emit_symbol_address(emitter, "x11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("ldr x11, [x11, x9, lsl #3]"); // load this class's property descriptor + emitter.instruction("ldr x12, [x11]"); // load the descriptor row count + emitter.instruction("cmp x1, x12"); // is the requested index within the row count? + emitter.instruction("b.hs __rt_obj_prop_name_none"); // past the last property → empty string + emitter.instruction(&format!("mov x13, #{}", PROP_DESC_ROW_BYTES)); // each descriptor row occupies 48 bytes + emitter.instruction("mul x13, x1, x13"); // byte offset of this property's row + emitter.instruction("add x13, x11, x13"); // advance into the descriptor + emitter.instruction("add x13, x13, #8"); // skip the leading row-count word + emitter.instruction("ldr x14, [x13, #16]"); // load the property's byte offset within the object + emitter.instruction("add x14, x0, x14"); // resolve the absolute property slot address + emitter.instruction("ldr x15, [x14, #8]"); // load the slot's high word (the init marker) + emit_uninit_sentinel_aarch64(emitter, "x16"); // materialize the uninitialized-property marker + emitter.instruction("cmp x15, x16"); // is this property still uninitialized? + emitter.instruction("b.eq __rt_obj_prop_name_none"); // PHP omits it from var_export output + emitter.instruction("ldr x2, [x13, #40]"); // load the bare property-name length + emitter.instruction("ldr x1, [x13, #32]"); // load the bare property-name pointer + emitter.instruction("ret"); // return to caller + emitter.label("__rt_obj_prop_name_none"); + abi::emit_symbol_address(emitter, "x1", "_class_name_missing"); // reuse the shared empty-name slot as the buffer + emitter.instruction("mov x2, #0"); // a zero-length string means "no property here" + emitter.instruction("ret"); // return to caller + } + Arch::X86_64 => { + emitter.instruction("test rdi, rdi"); // a non-object value has no property names + emitter.instruction("jz __rt_obj_prop_name_none_x86"); // yield the empty string + emitter.instruction("cmp rsi, 0"); // reject a negative index before scaling it + emitter.instruction("jl __rt_obj_prop_name_none_x86"); // out-of-range indices yield the empty string + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of registered class ids + emitter.instruction("cmp r9, r10"); // is the class id within the descriptor table? + emitter.instruction("jae __rt_obj_prop_name_none_x86"); // an unknown class has no property names + abi::emit_symbol_address(emitter, "r11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("mov r11, QWORD PTR [r11 + r9 * 8]"); // load this class's property descriptor + emitter.instruction("mov r10, QWORD PTR [r11]"); // load the descriptor row count + emitter.instruction("cmp rsi, r10"); // is the requested index within the row count? + emitter.instruction("jae __rt_obj_prop_name_none_x86"); // past the last property → empty string + emitter.instruction("mov rax, rsi"); // copy the index for row-offset scaling + let scale = format!("imul rax, rax, {}", PROP_DESC_ROW_BYTES); + emitter.instruction(&scale); // each descriptor row occupies 48 bytes + emitter.instruction("add rax, r11"); // advance into the descriptor + emitter.instruction("add rax, 8"); // skip the leading row-count word + emitter.instruction("mov r10, QWORD PTR [rax + 16]"); // load the property's byte offset within the object + emitter.instruction("add r10, rdi"); // resolve the absolute property slot address + emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load the slot's high word (the init marker) + emitter.instruction("movabs r8, 0x7ffffffffffffffd"); // materialize the uninitialized-property marker + emitter.instruction("cmp r10, r8"); // is this property still uninitialized? + emitter.instruction("je __rt_obj_prop_name_none_x86"); // PHP omits it from var_export output + emitter.instruction("mov rdx, QWORD PTR [rax + 40]"); // load the bare property-name length + emitter.instruction("mov rax, QWORD PTR [rax + 32]"); // load the bare property-name pointer + emitter.instruction("ret"); // return to caller + emitter.label("__rt_obj_prop_name_none_x86"); + abi::emit_symbol_address(emitter, "rax", "_class_name_missing"); // reuse the shared empty-name slot as the buffer + emitter.instruction("xor edx, edx"); // a zero-length string means "no property here" + emitter.instruction("ret"); // return to caller + } + } +} + +/// Materializes the uninitialized-typed-property sentinel into an AArch64 register. +/// +/// Must match `codegen_support::sentinels::UNINITIALIZED_TYPED_PROPERTY_SENTINEL` +/// (`0x7fff_ffff_ffff_fffd`) exactly, or a property that never got a value would be +/// exported as garbage instead of being skipped. +fn emit_uninit_sentinel_aarch64(emitter: &mut Emitter, reg: &str) { + emitter.instruction(&format!("movz {}, #0xfffd", reg)); // low halfword of the uninitialized-typed-property sentinel + emitter.instruction(&format!("movk {}, #0xffff, lsl #16", reg)); // second halfword of the uninitialized sentinel + emitter.instruction(&format!("movk {}, #0xffff, lsl #32", reg)); // third halfword of the uninitialized sentinel + emitter.instruction(&format!("movk {}, #0x7fff, lsl #48", reg)); // top halfword of the uninitialized sentinel +} + +/// `__rt_obj_prop_value`: value of an object's Nth renderable property, boxed. +/// +/// Input: AArch64 x0=object x1=index / x86_64 rdi=object rsi=index. +/// Output: AArch64 x0 / x86_64 rax = a FRESHLY allocated Mixed cell the caller +/// owns. Anything that is not a readable property boxes canonical PHP null. +pub fn emit_obj_prop_value(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_obj_prop_value_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: obj_prop_value ---"); + emitter.label_global("__rt_obj_prop_value"); + + // Frame (32 bytes): [16] saved x29, [24] saved x30. + emitter.instruction("sub sp, sp, #32"); // allocate the property-read frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish the property-read frame pointer + + emitter.instruction("cbz x0, __rt_obj_prop_value_null"); // a non-object value has no properties + emitter.instruction("cmp x1, #0"); // reject a negative index before scaling it + emitter.instruction("b.lt __rt_obj_prop_value_null"); // out-of-range indices box PHP null + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("ldr x10, [x10]"); // load the number of registered class ids + emitter.instruction("cmp x9, x10"); // is the class id within the descriptor table? + emitter.instruction("b.hs __rt_obj_prop_value_null"); // an unknown class has no readable properties + abi::emit_symbol_address(emitter, "x11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("ldr x11, [x11, x9, lsl #3]"); // load this class's property descriptor + emitter.instruction("ldr x12, [x11]"); // load the descriptor row count + emitter.instruction("cmp x1, x12"); // is the requested index within the row count? + emitter.instruction("b.hs __rt_obj_prop_value_null"); // past the last property → PHP null + emitter.instruction(&format!("mov x13, #{}", PROP_DESC_ROW_BYTES)); // each descriptor row occupies 48 bytes + emitter.instruction("mul x13, x1, x13"); // byte offset of this property's row + emitter.instruction("add x13, x11, x13"); // advance into the descriptor + emitter.instruction("add x13, x13, #8"); // skip the leading row-count word + emitter.instruction("ldr x14, [x13, #16]"); // load the property's byte offset within the object + emitter.instruction("add x14, x0, x14"); // resolve the absolute property slot address + emitter.instruction("ldr x2, [x14, #8]"); // load the slot high word (payload high / init marker) + emit_uninit_sentinel_aarch64(emitter, "x16"); // materialize the uninitialized-property marker + emitter.instruction("cmp x2, x16"); // is this property still uninitialized? + emitter.instruction("b.eq __rt_obj_prop_value_null"); // PHP omits it, so report PHP null + emitter.instruction("ldr x1, [x14]"); // load the slot low word (the payload) + emitter.instruction("ldr x0, [x13, #24]"); // load the property's runtime value tag + + emitter.instruction("cmp x0, #4"); // only pointer-shaped tags can carry a null payload + emitter.instruction("b.lt __rt_obj_prop_value_box"); // scalar payloads box exactly as stored + emit_branch_if_null_container(emitter, "x1", "x9", "__rt_obj_prop_value_null"); // a zero/sentinel pointer is PHP null + emitter.instruction("cmp x0, #7"); // does the slot already hold a boxed Mixed cell? + emitter.instruction("b.ne __rt_obj_prop_value_box"); // a direct payload boxes as-is + emitter.instruction("mov x0, x1"); // pass the existing cell to the unboxer + emitter.instruction("bl __rt_mixed_unbox"); // x0=inner tag, x1=lo, x2=hi — re-box for a fresh copy + + emitter.label("__rt_obj_prop_value_box"); + emitter.instruction("bl __rt_mixed_from_value"); // x0 = freshly owned Mixed cell + emitter.instruction("b __rt_obj_prop_value_done"); // return the boxed property value + + emitter.label("__rt_obj_prop_value_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = canonical PHP null + abi::emit_load_int_immediate(emitter, "x1", NULL_SENTINEL); // null payload uses the shared in-band sentinel + emitter.instruction("mov x2, #0"); // null carries no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box PHP null for the caller + + emitter.label("__rt_obj_prop_value_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the property-read frame + emitter.instruction("ret"); // return to caller +} + +/// Emits the Linux x86_64 boxed property reader. +fn emit_obj_prop_value_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: obj_prop_value ---"); + emitter.label_global("__rt_obj_prop_value"); + + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the property-read frame pointer + emitter.instruction("sub rsp, 16"); // allocate the property-read frame + + emitter.instruction("test rdi, rdi"); // a non-object value has no properties + emitter.instruction("jz __rt_obj_prop_value_null_x86"); // box PHP null + emitter.instruction("cmp rsi, 0"); // reject a negative index before scaling it + emitter.instruction("jl __rt_obj_prop_value_null_x86"); // out-of-range indices box PHP null + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of registered class ids + emitter.instruction("cmp r9, r10"); // is the class id within the descriptor table? + emitter.instruction("jae __rt_obj_prop_value_null_x86"); // an unknown class has no readable properties + abi::emit_symbol_address(emitter, "r11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("mov r11, QWORD PTR [r11 + r9 * 8]"); // load this class's property descriptor + emitter.instruction("mov r10, QWORD PTR [r11]"); // load the descriptor row count + emitter.instruction("cmp rsi, r10"); // is the requested index within the row count? + emitter.instruction("jae __rt_obj_prop_value_null_x86"); // past the last property → PHP null + emitter.instruction("mov rax, rsi"); // copy the index for row-offset scaling + emitter.instruction(&format!("imul rax, rax, {}", PROP_DESC_ROW_BYTES)); // each descriptor row occupies 48 bytes + emitter.instruction("add rax, r11"); // advance into the descriptor + emitter.instruction("add rax, 8"); // skip the leading row-count word + emitter.instruction("mov r10, QWORD PTR [rax + 16]"); // load the property's byte offset within the object + emitter.instruction("add r10, rdi"); // resolve the absolute property slot address + emitter.instruction("mov rsi, QWORD PTR [r10 + 8]"); // load the slot high word (payload high / init marker) + emitter.instruction("movabs r8, 0x7ffffffffffffffd"); // materialize the uninitialized-property marker + emitter.instruction("cmp rsi, r8"); // is this property still uninitialized? + emitter.instruction("je __rt_obj_prop_value_null_x86"); // PHP omits it, so report PHP null + emitter.instruction("mov rdi, QWORD PTR [r10]"); // load the slot low word (the payload) + emitter.instruction("mov rax, QWORD PTR [rax + 24]"); // load the property's runtime value tag + + emitter.instruction("cmp rax, 4"); // only pointer-shaped tags can carry a null payload + emitter.instruction("jl __rt_obj_prop_value_box_x86"); // scalar payloads box exactly as stored + emit_branch_if_null_container(emitter, "rdi", "r9", "__rt_obj_prop_value_null_x86"); // a zero/sentinel pointer is PHP null + emitter.instruction("cmp rax, 7"); // does the slot already hold a boxed Mixed cell? + emitter.instruction("jne __rt_obj_prop_value_box_x86"); // a direct payload boxes as-is + emitter.instruction("mov rax, rdi"); // pass the existing cell to the unboxer + emitter.instruction("call __rt_mixed_unbox"); // rax=inner tag, rdi=lo, rdx=hi + emitter.instruction("mov rsi, rdx"); // unbox reports the high word in rdx; boxing reads it from rsi + + emitter.label("__rt_obj_prop_value_box_x86"); + emitter.instruction("call __rt_mixed_from_value"); // rax = freshly owned Mixed cell + emitter.instruction("jmp __rt_obj_prop_value_done_x86"); // return the boxed property value + + emitter.label("__rt_obj_prop_value_null_x86"); + emitter.instruction("mov rax, 8"); // runtime tag 8 = canonical PHP null + abi::emit_load_int_immediate(emitter, "rdi", NULL_SENTINEL); // null payload uses the shared in-band sentinel + emitter.instruction("xor esi, esi"); // null carries no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box PHP null for the caller + + emitter.label("__rt_obj_prop_value_done_x86"); + emitter.instruction("add rsp, 16"); // release the property-read frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} diff --git a/src/codegen_support/runtime/objects/mod.rs b/src/codegen_support/runtime/objects/mod.rs index 8cd8c5f426..50d1262113 100644 --- a/src/codegen_support/runtime/objects/mod.rs +++ b/src/codegen_support/runtime/objects/mod.rs @@ -9,6 +9,8 @@ //! - Helper names are consumed directly by codegen paths for `stdClass` and JSON-decoded `Mixed` values. mod call_destructor; +mod enum_debug; +mod export_props; mod handles; mod mixed_array_append; mod mixed_array_fetch_for_write; @@ -16,9 +18,16 @@ mod mixed_array_get; mod mixed_array_set; mod mixed_cell_autovivify; mod new_by_name; +mod print_r_object; mod stdclass; pub(crate) use call_destructor::emit_call_object_destructor; +pub(crate) use enum_debug::{ + emit_obj_enum_case_name, emit_obj_enum_kind, emit_obj_enum_name_offset, + emit_var_dump_emit_enum_line, +}; +pub(crate) use export_props::{emit_obj_prop_count, emit_obj_prop_name, emit_obj_prop_value}; +pub(crate) use print_r_object::{emit_pr_obj_desc, emit_print_r_object}; pub(crate) use handles::{ emit_acquire_object_handle, emit_object_handles, object_handle_free_slots, object_handle_index_slots, diff --git a/src/codegen_support/runtime/objects/print_r_object.rs b/src/codegen_support/runtime/objects/print_r_object.rs new file mode 100644 index 0000000000..3df94abf44 --- /dev/null +++ b/src/codegen_support/runtime/objects/print_r_object.rs @@ -0,0 +1,421 @@ +//! Purpose: +//! Emits `__rt_print_r_object` (and its `__rt_pr_obj_desc` descriptor lookup): the +//! runtime walker that renders PHP `print_r` output for an OBJECT — +//! `C Object\n(\n[prop] => value\n)\n` — including the +//! ` Enum` / ` Enum:int` / ` Enum:string` header PHP gives an enum case and the +//! ` *RECURSION*` marker a revisited instance renders instead of a body. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::objects`. +//! - `__rt_pr_val_obj` (tag 6) in `runtime::io::print_r_walk`, for an object +//! nested inside an array, a hash, another object, or a boxed Mixed cell. +//! - `crate::codegen::lower_inst::builtins::debug::lower_print_r` for a +//! statically typed top-level object, with base indent 0. +//! +//! Key details: +//! - INDENT CONTRACT. `print_r` passes indents as call arguments (unlike +//! var_dump's `_vd_indent` global). `base` is the column of the `(` and `)` +//! lines, entries sit at `base + 4`, and a container VALUE inside an entry opens +//! at `base + 8` — exactly php-src's `print_hash(indent)` / `indent + 4` / +//! `php_print_zval_r_to_buf(indent + 8)`. The per-entry `\n` is written here, +//! which is what produces PHP's blank line after a nested `)`. +//! - Property enumeration is driven by `_class_prop_desc_ptrs[class_id]` emitted +//! by `crate::codegen_support::runtime::data::user`: a property count at offset +//! 0, then one 48-byte row per rendered property — +//! `(key_ptr, key_len, byte_offset, value_tag, plain_key_ptr, plain_key_len)`. +//! `key` already carries print_r's visibility annotation (`x`, `y:protected`, +//! `z:C:private`), so no visibility reasoning happens at runtime. The rows are +//! the SAME rows `_class_vd_desc_*` uses, so print_r and var_dump can never +//! disagree about an object's shape. +//! - Uninitialized typed properties are SKIPPED entirely (PHP omits them from +//! `print_r`, unlike var_dump which prints `uninitialized(T)`); the marker is +//! the `UNINITIALIZED_TYPED_PROPERTY_SENTINEL` in the slot's high word. +//! - RECURSION GUARD: this reuses var_dump's `_vd_seen` pointer stack through +//! `__rt_vd_seen_find` / `__rt_vd_seen_push` / `__rt_vd_seen_pop`. The two +//! renderers can never be walking at the same time (no PHP callback runs inside +//! either), and sharing the stack keeps one bound and one `*RECURSION*` policy. +//! PHP marks the object only around its BODY, so two sibling references to one +//! instance both render in full. +//! - KNOWN DIVERGENCE: dynamic (undeclared) properties are not rendered, because +//! they are not in the descriptor. This matches what elephc's `var_dump` +//! already does for the same objects. + +use crate::codegen_support::abi; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Byte width of one `_class_prop_desc_*` property row. +const PROP_DESC_ROW_BYTES: u64 = 48; + +/// `__rt_pr_obj_desc`: resolve an object's print_r/var_export property descriptor. +/// +/// Bounds-checks the header class id against `_class_gc_desc_count` so a stale or +/// synthetic id lands on the empty `_class_prop_desc_missing` descriptor instead +/// of reading past the table. +/// Input: AArch64 x0 / x86_64 rdi = object pointer. +/// Output: AArch64 x0 / x86_64 rax = descriptor pointer. +pub fn emit_pr_obj_desc(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: pr_obj_desc ---"); + emitter.label_global("__rt_pr_obj_desc"); + + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("ldr x10, [x10]"); // load the number of registered class ids + emitter.instruction("cmp x9, x10"); // is the class id within the descriptor table? + emitter.instruction("b.hs __rt_pr_obj_desc_missing"); // out-of-range ids fall back to the empty descriptor + abi::emit_symbol_address(emitter, "x11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("ldr x0, [x11, x9, lsl #3]"); // load this class's print_r descriptor + emitter.instruction("ret"); // return to caller + emitter.label("__rt_pr_obj_desc_missing"); + abi::emit_symbol_address(emitter, "x0", "_class_prop_desc_missing"); // fall back to the zero-property descriptor + emitter.instruction("ret"); // return to caller + } + Arch::X86_64 => { + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_gc_desc_count"); // resolve the class-id table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of registered class ids + emitter.instruction("cmp r9, r10"); // is the class id within the descriptor table? + emitter.instruction("jae __rt_pr_obj_desc_missing_x86"); // out-of-range ids fall back to the empty descriptor + abi::emit_symbol_address(emitter, "r11", "_class_prop_desc_ptrs"); // resolve the per-class descriptor pointer table + emitter.instruction("mov rax, QWORD PTR [r11 + r9 * 8]"); // load this class's print_r descriptor + emitter.instruction("ret"); // return to caller + emitter.label("__rt_pr_obj_desc_missing_x86"); + abi::emit_symbol_address(emitter, "rax", "_class_prop_desc_missing");// fall back to the zero-property descriptor + emitter.instruction("ret"); // return to caller + } + } +} + +/// `__rt_print_r_object`: render one object exactly as PHP's `print_r` does. +/// +/// Writes `C Object\n` (or the enum header), then the `(\n` … `)\n` +/// body with one `[key] => value\n` line per initialized declared +/// property, or ` *RECURSION*` when the instance is already being walked. +/// Input: AArch64 x0=object x1=base indent / x86_64 rdi=object rsi=base indent. +pub fn emit_print_r_object(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_print_r_object_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: print_r_object ---"); + emitter.label_global("__rt_print_r_object"); + + // Frame (96 bytes): [0] object ptr, [8] base indent, [16] entry indent, + // [24] descriptor ptr, [32] property count, [40] property index, + // [48] descriptor row ptr, [56] property slot ptr, [80] x29, [88] x30. + emitter.instruction("sub sp, sp, #96"); // allocate the object-walk frame + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #80"); // establish the object-walk frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the object pointer + emitter.instruction("str x1, [sp, #8]"); // save the paren base indent + emitter.instruction("add x9, x1, #4"); // entry indent = base + 4 + emitter.instruction("str x9, [sp, #16]"); // save the entry indent + + // -- header: the class name from the shared class-id → (ptr, len) table -- + emitter.instruction("ldr x9, [x0]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "x10", "_class_name_count"); // resolve the class-name table extent + emitter.instruction("ldr x10, [x10]"); // load the number of named class ids + emitter.instruction("cmp x9, x10"); // is the class id within the name table? + emitter.instruction("b.hs __rt_pr_obj_anon"); // an unknown class id writes no name + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); // resolve the class-name entry table + emitter.instruction("add x11, x11, x9, lsl #4"); // each entry is a 16-byte (ptr, len) pair + emitter.instruction("ldr x1, [x11]"); // load the class-name pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the class-name length + emitter.instruction("b __rt_pr_obj_name"); // write the resolved name + emitter.label("__rt_pr_obj_anon"); + abi::emit_symbol_address(emitter, "x1", "_class_name_missing"); // fall back to the empty class-name slot + emitter.instruction("mov x2, #0"); // a zero-length write emits nothing + emitter.label("__rt_pr_obj_name"); + emitter.instruction("bl __rt_pr_write"); // write the class name + + // -- header suffix: PHP writes ` Object` for a class and ` Enum[:type]` for an enum -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer + emitter.instruction("bl __rt_obj_enum_kind"); // x0 = 0 plain / 1 pure / 2 int-backed / 3 string-backed + emitter.instruction("cmp x0, #1"); // a pure enum prints ` Enum` + emitter.instruction("b.eq __rt_pr_obj_sfx_enum"); // select the bare enum suffix + emitter.instruction("cmp x0, #2"); // an int-backed enum prints ` Enum:int` + emitter.instruction("b.eq __rt_pr_obj_sfx_enum_int"); // select the int-backed enum suffix + emitter.instruction("cmp x0, #3"); // a string-backed enum prints ` Enum:string` + emitter.instruction("b.eq __rt_pr_obj_sfx_enum_str"); // select the string-backed enum suffix + abi::emit_symbol_address(emitter, "x1", "_pr_object_suffix"); // load the ` Object\n` header suffix + emitter.instruction("mov x2, #8"); // len(" Object\n") = 8 + emitter.instruction("b __rt_pr_obj_sfx_write"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum"); + abi::emit_symbol_address(emitter, "x1", "_pr_enum_suffix"); // load the ` Enum\n` header suffix + emitter.instruction("mov x2, #6"); // len(" Enum\n") = 6 + emitter.instruction("b __rt_pr_obj_sfx_write"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum_int"); + abi::emit_symbol_address(emitter, "x1", "_pr_enum_int_suffix"); // load the ` Enum:int\n` header suffix + emitter.instruction("mov x2, #10"); // len(" Enum:int\n") = 10 + emitter.instruction("b __rt_pr_obj_sfx_write"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum_str"); + abi::emit_symbol_address(emitter, "x1", "_pr_enum_str_suffix"); // load the ` Enum:string\n` header suffix + emitter.instruction("mov x2, #13"); // len(" Enum:string\n") = 13 + emitter.label("__rt_pr_obj_sfx_write"); + emitter.instruction("bl __rt_pr_write"); // write the header suffix + + // -- a revisited instance renders ` *RECURSION*` instead of a body -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer + emitter.instruction("bl __rt_vd_seen_find"); // is this object already on the walk stack? + emitter.instruction("cbnz x0, __rt_pr_obj_recursion"); // PHP renders a revisited object as *RECURSION* + emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer + emitter.instruction("bl __rt_vd_seen_push"); // mark the object as being walked + + emitter.instruction("ldr x0, [sp, #8]"); // base → open helper argument + emitter.instruction("bl __rt_print_r_open"); // write `(\n` + + emitter.instruction("ldr x0, [sp, #0]"); // reload the object pointer + emitter.instruction("bl __rt_pr_obj_desc"); // x0 = this class's print_r descriptor + emitter.instruction("str x0, [sp, #24]"); // save the descriptor pointer + emitter.instruction("ldr x9, [x0]"); // load the rendered property count + emitter.instruction("str x9, [sp, #32]"); // save the property count for the loop guard + emitter.instruction("str xzr, [sp, #40]"); // property index = 0 + + emitter.label("__rt_pr_obj_loop"); + emitter.instruction("ldr x9, [sp, #40]"); // reload the property index + emitter.instruction("ldr x10, [sp, #32]"); // reload the property count + emitter.instruction("cmp x9, x10"); // rendered every property? + emitter.instruction("b.ge __rt_pr_obj_done"); // walk complete + + // -- resolve this property's 48-byte descriptor row -- + emitter.instruction("ldr x11, [sp, #24]"); // reload the descriptor pointer + emitter.instruction(&format!("mov x12, #{}", PROP_DESC_ROW_BYTES)); // each descriptor row occupies 48 bytes + emitter.instruction("mul x12, x9, x12"); // byte offset of this property's row + emitter.instruction("add x11, x11, x12"); // advance into the descriptor + emitter.instruction("add x11, x11, #8"); // skip the leading property-count word + emitter.instruction("str x11, [sp, #48]"); // save the row pointer across the calls + + // -- resolve this property's 16-byte slot inside the instance -- + emitter.instruction("ldr x13, [x11, #16]"); // load the property's byte offset within the object + emitter.instruction("ldr x14, [sp, #0]"); // reload the object pointer + emitter.instruction("add x13, x14, x13"); // resolve the absolute property slot address + emitter.instruction("str x13, [sp, #56]"); // save the slot pointer across the calls + + // -- PHP omits an uninitialized typed property from print_r entirely -- + emitter.instruction("ldr x14, [x13, #8]"); // load the slot's high word (the init marker) + emit_uninit_sentinel_aarch64(emitter, "x15"); // materialize the uninitialized-property marker + emitter.instruction("cmp x14, x15"); // is this property still uninitialized? + emitter.instruction("b.eq __rt_pr_obj_next"); // skip it without emitting a line + + // -- emit `[KEY] => ` -- + emitter.instruction("ldr x0, [x11]"); // load the pre-rendered key pointer + emitter.instruction("ldr x1, [x11, #8]"); // load the pre-rendered key length + emitter.instruction("ldr x2, [sp, #16]"); // entry indent → key helper argument + emitter.instruction("bl __rt_print_r_str_key"); // write `[KEY] => ` + + // -- render the value; __rt_print_r_value unboxes Mixed cells and recurses -- + emitter.instruction("ldr x11, [sp, #48]"); // reload the descriptor row pointer + emitter.instruction("ldr x13, [sp, #56]"); // reload the property slot pointer + emitter.instruction("ldr x0, [x11, #24]"); // property value tag → value renderer + emitter.instruction("ldr x1, [x13]"); // slot low word → value renderer + emitter.instruction("ldr x2, [x13, #8]"); // slot high word → value renderer + emitter.instruction("cmp x0, #4"); // only pointer-shaped tags (4-7) can carry a null payload + emitter.instruction("b.lt __rt_pr_obj_value"); // scalar payloads keep their exact bit pattern + crate::codegen_support::sentinels::emit_branch_if_null_container( + emitter, + "x1", + "x9", + "__rt_pr_obj_value_null", + ); + emitter.instruction("b __rt_pr_obj_value"); // a real pointer renders through its own tag + emitter.label("__rt_pr_obj_value_null"); + emitter.instruction("mov x0, #8"); // canonical PHP null: print_r renders the empty string + emitter.label("__rt_pr_obj_value"); + emitter.instruction("ldr x3, [sp, #16]"); // entry indent + emitter.instruction("add x3, x3, #4"); // nested container base = entry indent + 4 + emitter.instruction("bl __rt_print_r_value"); // render the property value + + abi::emit_symbol_address(emitter, "x1", "_pr_nl"); // load the line terminator + emitter.instruction("mov x2, #1"); // len("\n") = 1 + emitter.instruction("bl __rt_pr_write"); // terminate the entry line + + emitter.label("__rt_pr_obj_next"); + emitter.instruction("ldr x9, [sp, #40]"); // reload the property index + emitter.instruction("add x9, x9, #1"); // advance to the next property + emitter.instruction("str x9, [sp, #40]"); // save the updated index + emitter.instruction("b __rt_pr_obj_loop"); // continue the walk + + emitter.label("__rt_pr_obj_done"); + emitter.instruction("ldr x0, [sp, #8]"); // base → close helper argument + emitter.instruction("bl __rt_print_r_close"); // write `)\n` + emitter.instruction("bl __rt_vd_seen_pop"); // the object is no longer on the walk stack + emitter.instruction("b __rt_pr_obj_exit"); // object rendered + + emitter.label("__rt_pr_obj_recursion"); + abi::emit_symbol_address(emitter, "x1", "_pr_recursion"); // load the ` *RECURSION*` marker + emitter.instruction("mov x2, #12"); // len(" *RECURSION*") = 12 + emitter.instruction("bl __rt_pr_write"); // write ` *RECURSION*` in place of the body + + emitter.label("__rt_pr_obj_exit"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the object-walk frame + emitter.instruction("ret"); // return to caller +} + +/// Materializes the uninitialized-typed-property sentinel into an AArch64 register. +/// +/// The value (`0x7fff_ffff_ffff_fffd`) needs the full four-halfword `movz`/`movk` +/// sequence and must match `codegen_support::sentinels:: +/// UNINITIALIZED_TYPED_PROPERTY_SENTINEL`, or an initialized property would be +/// silently dropped from the rendered body. +fn emit_uninit_sentinel_aarch64(emitter: &mut Emitter, reg: &str) { + emitter.instruction(&format!("movz {}, #0xfffd", reg)); // low halfword of the uninitialized-typed-property sentinel + emitter.instruction(&format!("movk {}, #0xffff, lsl #16", reg)); // second halfword of the uninitialized sentinel + emitter.instruction(&format!("movk {}, #0xffff, lsl #32", reg)); // third halfword of the uninitialized sentinel + emitter.instruction(&format!("movk {}, #0x7fff, lsl #48", reg)); // top halfword of the uninitialized sentinel +} + +/// Emits the Linux x86_64 print_r object walker. +fn emit_print_r_object_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: print_r_object ---"); + emitter.label_global("__rt_print_r_object"); + + // rbp-relative frame: [-8] object ptr, [-16] base indent, [-24] entry indent, + // [-32] descriptor ptr, [-40] property count, [-48] property index, + // [-56] descriptor row ptr, [-64] property slot ptr. + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the object-walk frame pointer + emitter.instruction("sub rsp, 80"); // allocate the object-walk frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the object pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the paren base indent + emitter.instruction("lea rax, [rsi + 4]"); // entry indent = base + 4 + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the entry indent + + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the runtime class id from the object header + abi::emit_symbol_address(emitter, "r10", "_class_name_count"); // resolve the class-name table extent + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the number of named class ids + emitter.instruction("cmp r9, r10"); // is the class id within the name table? + emitter.instruction("jae __rt_pr_obj_anon_x86"); // an unknown class id writes no name + abi::emit_symbol_address(emitter, "r11", "_class_name_entries"); // resolve the class-name entry table + emitter.instruction("shl r9, 4"); // each entry is a 16-byte (ptr, len) pair + emitter.instruction("add r11, r9"); // advance to this class's entry + emitter.instruction("mov rsi, QWORD PTR [r11]"); // load the class-name pointer + emitter.instruction("mov rdx, QWORD PTR [r11 + 8]"); // load the class-name length + emitter.instruction("jmp __rt_pr_obj_name_x86"); // write the resolved name + emitter.label("__rt_pr_obj_anon_x86"); + abi::emit_symbol_address(emitter, "rsi", "_class_name_missing"); // fall back to the empty class-name slot + emitter.instruction("xor edx, edx"); // a zero-length write emits nothing + emitter.label("__rt_pr_obj_name_x86"); + emitter.instruction("call __rt_pr_write"); // write the class name + + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the object pointer + emitter.instruction("call __rt_obj_enum_kind"); // rax = 0 plain / 1 pure / 2 int-backed / 3 string-backed + emitter.instruction("cmp rax, 1"); // a pure enum prints ` Enum` + emitter.instruction("je __rt_pr_obj_sfx_enum_x86"); // select the bare enum suffix + emitter.instruction("cmp rax, 2"); // an int-backed enum prints ` Enum:int` + emitter.instruction("je __rt_pr_obj_sfx_enum_int_x86"); // select the int-backed enum suffix + emitter.instruction("cmp rax, 3"); // a string-backed enum prints ` Enum:string` + emitter.instruction("je __rt_pr_obj_sfx_enum_str_x86"); // select the string-backed enum suffix + abi::emit_symbol_address(emitter, "rsi", "_pr_object_suffix"); // load the ` Object\n` header suffix + emitter.instruction("mov edx, 8"); // len(" Object\n") = 8 + emitter.instruction("jmp __rt_pr_obj_sfx_write_x86"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum_x86"); + abi::emit_symbol_address(emitter, "rsi", "_pr_enum_suffix"); // load the ` Enum\n` header suffix + emitter.instruction("mov edx, 6"); // len(" Enum\n") = 6 + emitter.instruction("jmp __rt_pr_obj_sfx_write_x86"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum_int_x86"); + abi::emit_symbol_address(emitter, "rsi", "_pr_enum_int_suffix"); // load the ` Enum:int\n` header suffix + emitter.instruction("mov edx, 10"); // len(" Enum:int\n") = 10 + emitter.instruction("jmp __rt_pr_obj_sfx_write_x86"); // write the selected suffix + emitter.label("__rt_pr_obj_sfx_enum_str_x86"); + abi::emit_symbol_address(emitter, "rsi", "_pr_enum_str_suffix"); // load the ` Enum:string\n` header suffix + emitter.instruction("mov edx, 13"); // len(" Enum:string\n") = 13 + emitter.label("__rt_pr_obj_sfx_write_x86"); + emitter.instruction("call __rt_pr_write"); // write the header suffix + + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the object pointer + emitter.instruction("call __rt_vd_seen_find"); // is this object already on the walk stack? + emitter.instruction("cmp rax, 0"); // a hit means we are inside this same instance + emitter.instruction("jne __rt_pr_obj_recursion_x86"); // PHP renders a revisited object as *RECURSION* + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the object pointer + emitter.instruction("call __rt_vd_seen_push"); // mark the object as being walked + + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // base → open helper argument + emitter.instruction("call __rt_print_r_open"); // write `(\n` + + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the object pointer + emitter.instruction("call __rt_pr_obj_desc"); // rax = this class's print_r descriptor + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the descriptor pointer + emitter.instruction("mov r9, QWORD PTR [rax]"); // load the rendered property count + emitter.instruction("mov QWORD PTR [rbp - 40], r9"); // save the property count for the loop guard + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // property index = 0 + + emitter.label("__rt_pr_obj_loop_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload the property index + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the property count + emitter.instruction("cmp r9, r10"); // rendered every property? + emitter.instruction("jge __rt_pr_obj_done_x86"); // walk complete + + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the descriptor pointer + emitter.instruction(&format!("imul r9, r9, {}", PROP_DESC_ROW_BYTES)); // each descriptor row occupies 48 bytes + emitter.instruction("add r11, r9"); // advance into the descriptor + emitter.instruction("add r11, 8"); // skip the leading property-count word + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row pointer across the calls + + emitter.instruction("mov r10, QWORD PTR [r11 + 16]"); // load the property's byte offset within the object + emitter.instruction("add r10, QWORD PTR [rbp - 8]"); // resolve the absolute property slot address + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the slot pointer across the calls + + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // load the slot's high word (the init marker) + emitter.instruction("movabs r8, 0x7ffffffffffffffd"); // materialize the uninitialized-property marker + emitter.instruction("cmp rax, r8"); // is this property still uninitialized? + emitter.instruction("je __rt_pr_obj_next_x86"); // skip it without emitting a line + + emitter.instruction("mov rdi, QWORD PTR [r11]"); // load the pre-rendered key pointer + emitter.instruction("mov rsi, QWORD PTR [r11 + 8]"); // load the pre-rendered key length + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // entry indent → key helper argument + emitter.instruction("call __rt_print_r_str_key"); // write `[KEY] => ` + + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload the descriptor row pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 64]"); // reload the property slot pointer + emitter.instruction("mov rdi, QWORD PTR [r11 + 24]"); // property value tag → value renderer + emitter.instruction("mov rsi, QWORD PTR [r10]"); // slot low word → value renderer + emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // slot high word → value renderer + emitter.instruction("cmp rdi, 4"); // only pointer-shaped tags (4-7) can carry a null payload + emitter.instruction("jl __rt_pr_obj_value_x86"); // scalar payloads keep their exact bit pattern + crate::codegen_support::sentinels::emit_branch_if_null_container( + emitter, + "rsi", + "r8", + "__rt_pr_obj_value_null_x86", + ); + emitter.instruction("jmp __rt_pr_obj_value_x86"); // a real pointer renders through its own tag + emitter.label("__rt_pr_obj_value_null_x86"); + emitter.instruction("mov rdi, 8"); // canonical PHP null: print_r renders the empty string + emitter.label("__rt_pr_obj_value_x86"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // entry indent + emitter.instruction("add rcx, 4"); // nested container base = entry indent + 4 + emitter.instruction("call __rt_print_r_value"); // render the property value + + abi::emit_symbol_address(emitter, "rsi", "_pr_nl"); // load the line terminator + emitter.instruction("mov edx, 1"); // len("\n") = 1 + emitter.instruction("call __rt_pr_write"); // terminate the entry line + + emitter.label("__rt_pr_obj_next_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload the property index + emitter.instruction("add r9, 1"); // advance to the next property + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the updated index + emitter.instruction("jmp __rt_pr_obj_loop_x86"); // continue the walk + + emitter.label("__rt_pr_obj_done_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // base → close helper argument + emitter.instruction("call __rt_print_r_close"); // write `)\n` + emitter.instruction("call __rt_vd_seen_pop"); // the object is no longer on the walk stack + emitter.instruction("jmp __rt_pr_obj_exit_x86"); // object rendered + + emitter.label("__rt_pr_obj_recursion_x86"); + abi::emit_symbol_address(emitter, "rsi", "_pr_recursion"); // load the ` *RECURSION*` marker + emitter.instruction("mov edx, 12"); // len(" *RECURSION*") = 12 + emitter.instruction("call __rt_pr_write"); // write ` *RECURSION*` in place of the body + + emitter.label("__rt_pr_obj_exit_x86"); + emitter.instruction("add rsp, 80"); // release the object-walk frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} diff --git a/src/codegen_support/runtime/round_mode.rs b/src/codegen_support/runtime/round_mode.rs new file mode 100644 index 0000000000..5a32d8d72f --- /dev/null +++ b/src/codegen_support/runtime/round_mode.rs @@ -0,0 +1,518 @@ +//! Purpose: +//! Emits `__rt_round_mode`, the shared runtime implementation of PHP's +//! `round($num, $precision, $mode)` for every rounding mode and every supported target. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()`. +//! - `crate::codegen::lower_inst::builtins::round_mode::lower_round_with_mode()`. +//! +//! Key details: +//! - The routine is a line-by-line port of php-src 8.4 `_php_math_round()` + +//! `php_round_helper()` (`ext/standard/math.c`), including the integral-part correction that +//! makes `round(1.005, 2)` answer `1.01` instead of `1.0`. Keeping the same structure is the +//! only way the tie-breaking modes agree with PHP on values such as `0.285` where the binary +//! double sits just below the decimal tie. +//! - Rounding modes are the php-src integers: 1 `HALF_UP`, 2 `HALF_DOWN`, 3 `HALF_EVEN`, +//! 4 `HALF_ODD`, 5 `CEILING`, 6 `FLOOR`, 7 `TOWARD_ZERO`, 8 `AWAY_FROM_ZERO`. The caller +//! validates the range and raises PHP's `ValueError`, so this helper trusts `1..=8`. +//! - DIVERGENCE (documented): php-src re-materializes the result through +//! `snprintf()` + `zend_strtod()` when `abs($precision) >= 23`. This helper always uses the +//! plain multiply/divide, which can differ by one ULP for those absurd precisions. The +//! `integral == 0` early return keeps the `0.0 * INF` case from producing `NAN`. +//! - ABI: AArch64 takes the value in `d0`, `$precision` in `x0`, `$mode` in `x1` and returns in +//! `d0`; x86_64 takes `xmm0`, `rdi`, `rsi` and returns in `xmm0`. `pow()` is the only libc +//! call and only for `abs($precision) > 22`, so the frame is set up unconditionally. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// IEEE-754 payload of `1e16`, php-src's "beyond our precision" cutoff in `_php_math_round()`. +const ONE_E16_BITS: u64 = 0x4341_C379_37E0_8000; + +/// IEEE-754 payload of `1.0`, used to build `copysign(1.0, integral)`. +const ONE_BITS: u64 = 0x3FF0_0000_0000_0000; + +/// IEEE-754 payload of `0.5`, used to build `copysign(0.5, integral)`. +const HALF_BITS: u64 = 0x3FE0_0000_0000_0000; + +/// IEEE-754 payload of `10.0`, the base of the decimal precision exponent. +const TEN_BITS: u64 = 0x4024_0000_0000_0000; + +/// Emits the `__rt_round_mode` runtime helper for the active target. +/// +/// # Input +/// - AArch64: `d0` = `$num`, `x0` = `$precision`, `x1` = `$mode`. +/// - x86_64: `xmm0` = `$num`, `rdi` = `$precision`, `rsi` = `$mode`. +/// +/// # Output +/// - The rounded double in `d0` / `xmm0`. +/// +/// # Clobbers +/// - Caller-saved integer and floating-point registers, exactly like any other `__rt_*` call. +pub fn emit_round_mode(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_round_mode_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: round_mode ---"); + emitter.label_global("__rt_round_mode"); + + // Frame layout: + // [sp, #0] = $num + // [sp, #16] = $precision + // [sp, #24] = $mode + // [sp, #48] = saved x29/x30 + emitter.instruction("sub sp, sp, #64"); // allocate the round-mode frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set up the round-mode frame pointer + emitter.instruction("str d0, [sp, #0]"); // spill $num across the optional pow() call + emitter.instruction("str x0, [sp, #16]"); // spill $precision across the optional pow() call + emitter.instruction("str x1, [sp, #24]"); // spill $mode across the optional pow() call + + // -- php-src returns non-finite and zero inputs untouched -- + emitter.instruction("fmov x9, d0"); // raw IEEE-754 payload of $num + emitter.instruction("lsl x10, x9, #1"); // drop the sign bit so +0.0 and -0.0 collapse + emitter.instruction("cbz x10, __rt_round_mode_return_value"); // PHP returns +/-0.0 unchanged + emitter.instruction("movz x11, #0xffe0, lsl #48"); // smallest sign-stripped payload with an all-ones exponent + emitter.instruction("cmp x10, x11"); // is the exponent field saturated (INF or NAN)? + emitter.instruction("b.hs __rt_round_mode_return_value"); // PHP returns INF/NAN unchanged + + // -- exponent = php_intpow10(abs($precision)) -- + emitter.instruction("ldr x0, [sp, #16]"); // x0 = $precision + emitter.instruction("cmp x0, #0"); // is the requested precision negative? + emitter.instruction("cneg x0, x0, lt"); // x0 = abs($precision) + emitter.instruction("cmp x0, #22"); // 10^0..10^22 are exactly representable doubles + emitter.instruction("b.gt __rt_round_mode_pow_call"); // anything larger needs libc pow() + emitter.instruction("fmov d1, #1.0"); // start the exact power-of-ten accumulator at 1.0 + emitter.instruction("fmov d2, #10.0"); // the decimal base multiplied in per requested place + emitter.label("__rt_round_mode_pow_loop"); + emitter.instruction("cbz x0, __rt_round_mode_pow_done"); // every requested decimal place has been applied + emitter.instruction("fmul d1, d1, d2"); // multiply in one exact decimal place + emitter.instruction("sub x0, x0, #1"); // one decimal place consumed + emitter.instruction("b __rt_round_mode_pow_loop"); // keep accumulating the exact power of ten + emitter.label("__rt_round_mode_pow_call"); + emitter.instruction("scvtf d1, x0"); // convert abs($precision) into pow()'s exponent argument + emitter.instruction("fmov d0, #10.0"); // pow() base 10.0 + emitter.bl_c("pow"); + emitter.instruction("fmov d1, d0"); // move the computed exponent into the shared register + emitter.label("__rt_round_mode_pow_done"); + + // -- scale $num into the integral domain of the requested precision -- + emitter.instruction("ldr d0, [sp, #0]"); // d0 = $num + emitter.instruction("ldr x9, [sp, #16]"); // x9 = $precision, live for every later branch + emitter.instruction("cmp x9, #0"); // php-src branches on `places > 0` everywhere + emitter.instruction("b.gt __rt_round_mode_scale_mul"); // positive precision multiplies by the exponent + emitter.instruction("fdiv d2, d0, d1"); // scale by dividing for zero/negative precision + emitter.instruction("b __rt_round_mode_scale_done"); // the scaled value is ready + emitter.label("__rt_round_mode_scale_mul"); + emitter.instruction("fmul d2, d0, d1"); // scale by multiplying for positive precision + + // -- extract the integral part and php-src's off-by-one-ULP correction candidate -- + emitter.label("__rt_round_mode_scale_done"); + emitter.instruction("fcmp d0, #0.0"); // php-src splits on the sign of the original value + emitter.instruction("b.mi __rt_round_mode_negative"); // negative values take the ceil() branch + emitter.instruction("frintm d3, d2"); // integral = floor(scaled) + emitter.instruction("fmov d4, #1.0"); // the correction candidate is one step away + emitter.instruction("fadd d4, d3, d4"); // candidate = integral + 1.0 + emitter.instruction("b __rt_round_mode_correct"); // test whether the candidate rebuilds $num exactly + emitter.label("__rt_round_mode_negative"); + emitter.instruction("frintp d3, d2"); // integral = ceil(scaled) + emitter.instruction("fmov d4, #1.0"); // the correction candidate is one step away + emitter.instruction("fsub d4, d3, d4"); // candidate = integral - 1.0 + + // -- adopt the candidate when unscaling it reproduces $num bit-for-bit -- + emitter.label("__rt_round_mode_correct"); + emitter.instruction("cmp x9, #0"); // unscale the candidate the same way it was scaled + emitter.instruction("b.gt __rt_round_mode_back_div"); // positive precision unscales by dividing + emitter.instruction("fmul d5, d4, d1"); // unscale the candidate by multiplying + emitter.instruction("b __rt_round_mode_back_done"); // the unscaled candidate is ready + emitter.label("__rt_round_mode_back_div"); + emitter.instruction("fdiv d5, d4, d1"); // unscale the candidate by dividing + emitter.label("__rt_round_mode_back_done"); + emitter.instruction("fcmp d5, d0"); // did the candidate rebuild $num exactly? + emitter.instruction("fcsel d3, d4, d3, eq"); // adopt the corrected integral part when it did + + // -- values past the double precision limit are returned untouched -- + emitter.instruction("fabs d6, d3"); // magnitude of the integral part + emitter.instruction("movz x10, #0x4341, lsl #48"); // build 1e16, php-src's precision cutoff + emitter.instruction(&format!("movk x10, #0x{:04x}, lsl #32", (ONE_E16_BITS >> 32) & 0xFFFF)); // second halfword of 1e16 + emitter.instruction(&format!("movk x10, #0x{:04x}, lsl #16", (ONE_E16_BITS >> 16) & 0xFFFF)); // third halfword of 1e16 + emitter.instruction(&format!("movk x10, #0x{:04x}", ONE_E16_BITS & 0xFFFF)); // low halfword of 1e16 + emitter.instruction("fmov d7, x10"); // d7 = 1e16 + emitter.instruction("fcmp d6, d7"); // is the integral part beyond double precision? + emitter.instruction("b.ge __rt_round_mode_return_value"); // yes - php-src returns $num unchanged + + // -- php_round_helper(): dispatch on the requested rounding mode -- + emitter.instruction("fabs d6, d0"); // d6 = fabs($num), php-src's `value_abs` + emitter.instruction("ldr x11, [sp, #24]"); // x11 = $mode + emitter.instruction("cmp x11, #7"); // mode 7 = TOWARD_ZERO + emitter.instruction("b.eq __rt_round_mode_finish"); // truncation keeps the integral part as-is + emitter.instruction("fmov x10, d3"); // raw payload of the integral part + emitter.instruction("and x10, x10, #0x8000000000000000"); // isolate its sign bit for the copysign() builds + emitter.instruction(&format!("movz x12, #0x{:04x}, lsl #48", ONE_BITS >> 48)); // high halfword of 1.0 + emitter.instruction("orr x12, x10, x12"); // copysign(1.0, integral) + emitter.instruction("fmov d7, x12"); // d7 = the magnitude step php-src adds + emitter.instruction("cmp x11, #5"); // modes 5, 6 and 8 use the zero edge case + emitter.instruction("b.ge __rt_round_mode_zero_edge"); // directional modes skip the half-way edge case + + // -- php_round_get_basic_edge_case(): the exact half-way point of this integral step -- + emitter.instruction(&format!("movz x12, #0x{:04x}, lsl #48", HALF_BITS >> 48)); // high halfword of 0.5 + emitter.instruction("orr x12, x10, x12"); // copysign(0.5, integral) + emitter.instruction("fmov d2, x12"); // d2 = the half-way offset + emitter.instruction("fadd d2, d3, d2"); // integral + copysign(0.5, integral) + emitter.instruction("cmp x9, #0"); // unscale the edge case like php-src does + emitter.instruction("b.gt __rt_round_mode_edge_div"); // positive precision unscales by dividing + emitter.instruction("fmul d2, d2, d1"); // unscale the edge case by multiplying + emitter.instruction("b __rt_round_mode_edge_done"); // the edge case is ready + emitter.label("__rt_round_mode_edge_div"); + emitter.instruction("fdiv d2, d2, d1"); // unscale the edge case by dividing + emitter.label("__rt_round_mode_edge_done"); + emitter.instruction("fabs d2, d2"); // php-src compares magnitudes only + emitter.instruction("cmp x11, #1"); // mode 1 = HALF_UP + emitter.instruction("b.eq __rt_round_mode_half_up"); // ties move away from zero + emitter.instruction("cmp x11, #2"); // mode 2 = HALF_DOWN + emitter.instruction("b.eq __rt_round_mode_half_down"); // ties move toward zero + emitter.instruction("cmp x11, #3"); // mode 3 = HALF_EVEN + emitter.instruction("b.eq __rt_round_mode_half_even"); // ties move to the even neighbour + emitter.instruction("b __rt_round_mode_half_odd"); // mode 4 = HALF_ODD + + emitter.label("__rt_round_mode_half_up"); + emitter.instruction("fcmp d6, d2"); // compare $num against the half-way point + emitter.instruction("b.ge __rt_round_mode_bump"); // ties and everything above round away from zero + emitter.instruction("b __rt_round_mode_finish"); // below the half-way point the integral part stands + + emitter.label("__rt_round_mode_half_down"); + emitter.instruction("fcmp d6, d2"); // compare $num against the half-way point + emitter.instruction("b.gt __rt_round_mode_bump"); // only strictly-above rounds away from zero + emitter.instruction("b __rt_round_mode_finish"); // ties stay on the integral part + + emitter.label("__rt_round_mode_half_even"); + emitter.instruction("fcmp d6, d2"); // compare $num against the half-way point + emitter.instruction("b.gt __rt_round_mode_bump"); // strictly above the tie always rounds away + emitter.instruction("b.ne __rt_round_mode_finish"); // strictly below the tie keeps the integral part + emitter.instruction("fcvtzs x13, d3"); // the integral part is exact below 1e16 + emitter.instruction("tbnz x13, #0, __rt_round_mode_bump"); // an odd integral part must step to the even neighbour + emitter.instruction("b __rt_round_mode_finish"); // an even integral part is already correct + + emitter.label("__rt_round_mode_half_odd"); + emitter.instruction("fcmp d6, d2"); // compare $num against the half-way point + emitter.instruction("b.gt __rt_round_mode_bump"); // strictly above the tie always rounds away + emitter.instruction("b.ne __rt_round_mode_finish"); // strictly below the tie keeps the integral part + emitter.instruction("fcvtzs x13, d3"); // the integral part is exact below 1e16 + emitter.instruction("tbz x13, #0, __rt_round_mode_bump"); // an even integral part must step to the odd neighbour + emitter.instruction("b __rt_round_mode_finish"); // an odd integral part is already correct + + // -- php_round_get_zero_edge_case(): the directional modes compare against the step itself -- + emitter.label("__rt_round_mode_zero_edge"); + emitter.instruction("cmp x9, #0"); // unscale the integral part like php-src does + emitter.instruction("b.gt __rt_round_mode_zero_edge_div"); // positive precision unscales by dividing + emitter.instruction("fmul d2, d3, d1"); // unscale the integral part by multiplying + emitter.instruction("b __rt_round_mode_zero_edge_done"); // the zero edge case is ready + emitter.label("__rt_round_mode_zero_edge_div"); + emitter.instruction("fdiv d2, d3, d1"); // unscale the integral part by dividing + emitter.label("__rt_round_mode_zero_edge_done"); + emitter.instruction("fabs d2, d2"); // php-src compares magnitudes only + emitter.instruction("cmp x11, #5"); // mode 5 = CEILING + emitter.instruction("b.eq __rt_round_mode_ceiling"); // round toward positive infinity + emitter.instruction("cmp x11, #6"); // mode 6 = FLOOR + emitter.instruction("b.eq __rt_round_mode_floor"); // round toward negative infinity + emitter.instruction("fcmp d6, d2"); // mode 8 = AWAY_FROM_ZERO + emitter.instruction("b.gt __rt_round_mode_bump"); // any remainder grows the magnitude + emitter.instruction("b __rt_round_mode_finish"); // an exact value keeps the integral part + + emitter.label("__rt_round_mode_ceiling"); + emitter.instruction("fcmp d0, #0.0"); // CEILING only moves strictly positive values + emitter.instruction("b.ls __rt_round_mode_finish"); // non-positive values already sit at the ceiling + emitter.instruction("fcmp d6, d2"); // is there any remainder left to round away? + emitter.instruction("b.ls __rt_round_mode_finish"); // an exact value keeps the integral part + emitter.instruction("fmov d7, #1.0"); // CEILING always adds +1.0, never copysign() + emitter.instruction("b __rt_round_mode_bump"); // step toward positive infinity + + emitter.label("__rt_round_mode_floor"); + emitter.instruction("fcmp d0, #0.0"); // FLOOR only moves strictly negative values + emitter.instruction("b.ge __rt_round_mode_finish"); // non-negative values already sit at the floor + emitter.instruction("fcmp d6, d2"); // is there any remainder left to round away? + emitter.instruction("b.ls __rt_round_mode_finish"); // an exact value keeps the integral part + emitter.instruction("fmov d7, #-1.0"); // FLOOR always subtracts 1.0, never copysign() + + emitter.label("__rt_round_mode_bump"); + emitter.instruction("fadd d3, d3, d7"); // move the integral part one step in the chosen direction + + // -- unscale the rounded integral part back to the requested precision -- + emitter.label("__rt_round_mode_finish"); + emitter.instruction("fcmp d3, #0.0"); // a zero integral part already carries the final sign + emitter.instruction("b.eq __rt_round_mode_return_integral"); // avoid 0.0 * INF turning an absurd precision into NAN + emitter.instruction("cmp x9, #0"); // unscale the result the way php-src does + emitter.instruction("b.gt __rt_round_mode_result_div"); // positive precision unscales by dividing + emitter.instruction("fmul d0, d3, d1"); // unscale the rounded value by multiplying + emitter.instruction("b __rt_round_mode_return"); // the rounded result is ready + emitter.label("__rt_round_mode_result_div"); + emitter.instruction("fdiv d0, d3, d1"); // unscale the rounded value by dividing + emitter.instruction("b __rt_round_mode_return"); // the rounded result is ready + + emitter.label("__rt_round_mode_return_integral"); + emitter.instruction("fmov d0, d3"); // return the signed zero unchanged + emitter.instruction("b __rt_round_mode_return"); // fall through to the shared epilogue + + emitter.label("__rt_round_mode_return_value"); + emitter.instruction("ldr d0, [sp, #0]"); // PHP returns the untouched $num for these inputs + + emitter.label("__rt_round_mode_return"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the round-mode frame + emitter.instruction("ret"); // return with d0 = rounded value +} + +/// Emits the x86_64 System V variant of `__rt_round_mode`. +/// +/// Mirrors the AArch64 logic instruction for instruction; only the register convention, the +/// 64-bit immediate materialization, and the SSE spelling of `floor`/`ceil`/`fabs` differ. +/// `roundsd` requires SSE4.1, which the backend already assumes for `floor()`/`ceil()`. +fn emit_round_mode_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: round_mode ---"); + emitter.label_global("__rt_round_mode"); + + // Frame layout: + // [rbp - 8] = $num + // [rbp - 16] = $precision + // [rbp - 24] = $mode + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the spill slots + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots so pow() stays 16-byte aligned + emitter.instruction("movsd QWORD PTR [rbp - 8], xmm0"); // spill $num across the optional pow() call + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // spill $precision across the optional pow() call + emitter.instruction("mov QWORD PTR [rbp - 24], rsi"); // spill $mode across the optional pow() call + + // -- php-src returns non-finite and zero inputs untouched -- + emitter.instruction("movq rax, xmm0"); // raw IEEE-754 payload of $num + emitter.instruction("add rax, rax"); // drop the sign bit so +0.0 and -0.0 collapse + emitter.instruction("je __rt_round_mode_return_value_x86"); // PHP returns +/-0.0 unchanged + emitter.instruction("mov rcx, 0xffe0000000000000"); // smallest sign-stripped payload with an all-ones exponent + emitter.instruction("cmp rax, rcx"); // is the exponent field saturated (INF or NAN)? + emitter.instruction("jae __rt_round_mode_return_value_x86"); // PHP returns INF/NAN unchanged + + // -- exponent = php_intpow10(abs($precision)) -- + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // rax = $precision + emitter.instruction("mov rdx, rax"); // copy it to build the arithmetic absolute value + emitter.instruction("sar rdx, 63"); // expand the sign bit into an all-zero or all-one mask + emitter.instruction("xor rax, rdx"); // flip the payload bits for negative precisions + emitter.instruction("sub rax, rdx"); // rax = abs($precision) + emitter.instruction("cmp rax, 22"); // 10^0..10^22 are exactly representable doubles + emitter.instruction("jg __rt_round_mode_pow_call_x86"); // anything larger needs libc pow() + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_BITS)); // IEEE-754 payload of 1.0 + emitter.instruction("movq xmm1, rcx"); // start the exact power-of-ten accumulator at 1.0 + emitter.instruction(&format!("mov rcx, 0x{:x}", TEN_BITS)); // IEEE-754 payload of 10.0 + emitter.instruction("movq xmm2, rcx"); // the decimal base multiplied in per requested place + emitter.label("__rt_round_mode_pow_loop_x86"); + emitter.instruction("test rax, rax"); // every requested decimal place applied? + emitter.instruction("je __rt_round_mode_pow_done_x86"); // yes - the exact exponent is ready + emitter.instruction("mulsd xmm1, xmm2"); // multiply in one exact decimal place + emitter.instruction("dec rax"); // one decimal place consumed + emitter.instruction("jmp __rt_round_mode_pow_loop_x86"); // keep accumulating the exact power of ten + emitter.label("__rt_round_mode_pow_call_x86"); + emitter.instruction("cvtsi2sd xmm1, rax"); // convert abs($precision) into pow()'s exponent argument + emitter.instruction(&format!("mov rcx, 0x{:x}", TEN_BITS)); // IEEE-754 payload of 10.0 + emitter.instruction("movq xmm0, rcx"); // pow() base 10.0 + emitter.bl_c("pow"); + emitter.instruction("movsd xmm1, xmm0"); // move the computed exponent into the shared register + emitter.label("__rt_round_mode_pow_done_x86"); + + // -- scale $num into the integral domain of the requested precision -- + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // xmm0 = $num + emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // r9 = $precision, live for every later branch + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // r10 = $mode, live for every later branch + emitter.instruction("movsd xmm2, xmm0"); // xmm2 = the value being scaled + emitter.instruction("cmp r9, 0"); // php-src branches on `places > 0` everywhere + emitter.instruction("jg __rt_round_mode_scale_mul_x86"); // positive precision multiplies by the exponent + emitter.instruction("divsd xmm2, xmm1"); // scale by dividing for zero/negative precision + emitter.instruction("jmp __rt_round_mode_scale_done_x86"); // the scaled value is ready + emitter.label("__rt_round_mode_scale_mul_x86"); + emitter.instruction("mulsd xmm2, xmm1"); // scale by multiplying for positive precision + + // -- extract the integral part and php-src's off-by-one-ULP correction candidate -- + emitter.label("__rt_round_mode_scale_done_x86"); + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_BITS)); // IEEE-754 payload of 1.0 + emitter.instruction("movq xmm5, rcx"); // xmm5 = 1.0, the correction step + emitter.instruction("xorpd xmm7, xmm7"); // xmm7 = 0.0 for the sign test + emitter.instruction("comisd xmm0, xmm7"); // php-src splits on the sign of the original value + emitter.instruction("jb __rt_round_mode_negative_x86"); // negative values take the ceil() branch + emitter.instruction("roundsd xmm3, xmm2, 1"); // integral = floor(scaled) + emitter.instruction("movsd xmm4, xmm3"); // copy the integral part for the candidate + emitter.instruction("addsd xmm4, xmm5"); // candidate = integral + 1.0 + emitter.instruction("jmp __rt_round_mode_correct_x86"); // test whether the candidate rebuilds $num exactly + emitter.label("__rt_round_mode_negative_x86"); + emitter.instruction("roundsd xmm3, xmm2, 2"); // integral = ceil(scaled) + emitter.instruction("movsd xmm4, xmm3"); // copy the integral part for the candidate + emitter.instruction("subsd xmm4, xmm5"); // candidate = integral - 1.0 + + // -- adopt the candidate when unscaling it reproduces $num bit-for-bit -- + emitter.label("__rt_round_mode_correct_x86"); + emitter.instruction("movsd xmm5, xmm4"); // xmm5 = the candidate being unscaled + emitter.instruction("cmp r9, 0"); // unscale the candidate the same way it was scaled + emitter.instruction("jg __rt_round_mode_back_div_x86"); // positive precision unscales by dividing + emitter.instruction("mulsd xmm5, xmm1"); // unscale the candidate by multiplying + emitter.instruction("jmp __rt_round_mode_back_done_x86"); // the unscaled candidate is ready + emitter.label("__rt_round_mode_back_div_x86"); + emitter.instruction("divsd xmm5, xmm1"); // unscale the candidate by dividing + emitter.label("__rt_round_mode_back_done_x86"); + emitter.instruction("ucomisd xmm5, xmm0"); // did the candidate rebuild $num exactly? + emitter.instruction("jp __rt_round_mode_no_correct_x86"); // an unordered compare is never an exact match + emitter.instruction("jne __rt_round_mode_no_correct_x86"); // a different value leaves the integral part alone + emitter.instruction("movsd xmm3, xmm4"); // adopt the corrected integral part + + // -- values past the double precision limit are returned untouched -- + emitter.label("__rt_round_mode_no_correct_x86"); + emitter.instruction("movq rcx, xmm3"); // raw payload of the integral part + emitter.instruction("shl rcx, 1"); // drop the sign bit to take the magnitude + emitter.instruction("shr rcx, 1"); // restore the exponent/mantissa alignment + emitter.instruction("movq xmm6, rcx"); // xmm6 = fabs(integral) + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_E16_BITS)); // 1e16, php-src's precision cutoff + emitter.instruction("movq xmm7, rcx"); // xmm7 = 1e16 + emitter.instruction("comisd xmm6, xmm7"); // is the integral part beyond double precision? + emitter.instruction("jae __rt_round_mode_return_value_x86"); // yes - php-src returns $num unchanged + + // -- php_round_helper(): dispatch on the requested rounding mode -- + emitter.instruction("movq rcx, xmm0"); // raw payload of $num + emitter.instruction("shl rcx, 1"); // drop the sign bit to take the magnitude + emitter.instruction("shr rcx, 1"); // restore the exponent/mantissa alignment + emitter.instruction("movq xmm6, rcx"); // xmm6 = fabs($num), php-src's `value_abs` + emitter.instruction("cmp r10, 7"); // mode 7 = TOWARD_ZERO + emitter.instruction("je __rt_round_mode_finish_x86"); // truncation keeps the integral part as-is + emitter.instruction("movq rdx, xmm3"); // raw payload of the integral part + emitter.instruction("mov rcx, 0x8000000000000000"); // IEEE-754 sign-bit mask + emitter.instruction("and rdx, rcx"); // isolate the sign bit for the copysign() builds + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_BITS)); // IEEE-754 payload of 1.0 + emitter.instruction("mov r8, rdx"); // copy the isolated sign bit + emitter.instruction("or r8, rcx"); // copysign(1.0, integral) + emitter.instruction("movq xmm7, r8"); // xmm7 = the magnitude step php-src adds + emitter.instruction("cmp r10, 5"); // modes 5, 6 and 8 use the zero edge case + emitter.instruction("jge __rt_round_mode_zero_edge_x86"); // directional modes skip the half-way edge case + + // -- php_round_get_basic_edge_case(): the exact half-way point of this integral step -- + emitter.instruction(&format!("mov rcx, 0x{:x}", HALF_BITS)); // IEEE-754 payload of 0.5 + emitter.instruction("or rdx, rcx"); // copysign(0.5, integral) + emitter.instruction("movq xmm2, rdx"); // xmm2 = the half-way offset + emitter.instruction("addsd xmm2, xmm3"); // integral + copysign(0.5, integral) + emitter.instruction("cmp r9, 0"); // unscale the edge case like php-src does + emitter.instruction("jg __rt_round_mode_edge_div_x86"); // positive precision unscales by dividing + emitter.instruction("mulsd xmm2, xmm1"); // unscale the edge case by multiplying + emitter.instruction("jmp __rt_round_mode_edge_done_x86"); // the edge case is ready + emitter.label("__rt_round_mode_edge_div_x86"); + emitter.instruction("divsd xmm2, xmm1"); // unscale the edge case by dividing + emitter.label("__rt_round_mode_edge_done_x86"); + emitter.instruction("movq rcx, xmm2"); // raw payload of the edge case + emitter.instruction("shl rcx, 1"); // drop the sign bit to take the magnitude + emitter.instruction("shr rcx, 1"); // restore the exponent/mantissa alignment + emitter.instruction("movq xmm2, rcx"); // php-src compares magnitudes only + emitter.instruction("cmp r10, 1"); // mode 1 = HALF_UP + emitter.instruction("je __rt_round_mode_half_up_x86"); // ties move away from zero + emitter.instruction("cmp r10, 2"); // mode 2 = HALF_DOWN + emitter.instruction("je __rt_round_mode_half_down_x86"); // ties move toward zero + emitter.instruction("cmp r10, 3"); // mode 3 = HALF_EVEN + emitter.instruction("je __rt_round_mode_half_even_x86"); // ties move to the even neighbour + emitter.instruction("jmp __rt_round_mode_half_odd_x86"); // mode 4 = HALF_ODD + + emitter.label("__rt_round_mode_half_up_x86"); + emitter.instruction("comisd xmm6, xmm2"); // compare $num against the half-way point + emitter.instruction("jae __rt_round_mode_bump_x86"); // ties and everything above round away from zero + emitter.instruction("jmp __rt_round_mode_finish_x86"); // below the half-way point the integral part stands + + emitter.label("__rt_round_mode_half_down_x86"); + emitter.instruction("comisd xmm6, xmm2"); // compare $num against the half-way point + emitter.instruction("ja __rt_round_mode_bump_x86"); // only strictly-above rounds away from zero + emitter.instruction("jmp __rt_round_mode_finish_x86"); // ties stay on the integral part + + emitter.label("__rt_round_mode_half_even_x86"); + emitter.instruction("comisd xmm6, xmm2"); // compare $num against the half-way point + emitter.instruction("ja __rt_round_mode_bump_x86"); // strictly above the tie always rounds away + emitter.instruction("jne __rt_round_mode_finish_x86"); // strictly below the tie keeps the integral part + emitter.instruction("cvttsd2si r11, xmm3"); // the integral part is exact below 1e16 + emitter.instruction("test r11, 1"); // is the integral part odd? + emitter.instruction("jne __rt_round_mode_bump_x86"); // an odd integral part must step to the even neighbour + emitter.instruction("jmp __rt_round_mode_finish_x86"); // an even integral part is already correct + + emitter.label("__rt_round_mode_half_odd_x86"); + emitter.instruction("comisd xmm6, xmm2"); // compare $num against the half-way point + emitter.instruction("ja __rt_round_mode_bump_x86"); // strictly above the tie always rounds away + emitter.instruction("jne __rt_round_mode_finish_x86"); // strictly below the tie keeps the integral part + emitter.instruction("cvttsd2si r11, xmm3"); // the integral part is exact below 1e16 + emitter.instruction("test r11, 1"); // is the integral part odd? + emitter.instruction("je __rt_round_mode_bump_x86"); // an even integral part must step to the odd neighbour + emitter.instruction("jmp __rt_round_mode_finish_x86"); // an odd integral part is already correct + + // -- php_round_get_zero_edge_case(): the directional modes compare against the step itself -- + emitter.label("__rt_round_mode_zero_edge_x86"); + emitter.instruction("movsd xmm2, xmm3"); // xmm2 = the integral part being unscaled + emitter.instruction("cmp r9, 0"); // unscale the integral part like php-src does + emitter.instruction("jg __rt_round_mode_zero_edge_div_x86"); // positive precision unscales by dividing + emitter.instruction("mulsd xmm2, xmm1"); // unscale the integral part by multiplying + emitter.instruction("jmp __rt_round_mode_zero_edge_done_x86"); // the zero edge case is ready + emitter.label("__rt_round_mode_zero_edge_div_x86"); + emitter.instruction("divsd xmm2, xmm1"); // unscale the integral part by dividing + emitter.label("__rt_round_mode_zero_edge_done_x86"); + emitter.instruction("movq rcx, xmm2"); // raw payload of the zero edge case + emitter.instruction("shl rcx, 1"); // drop the sign bit to take the magnitude + emitter.instruction("shr rcx, 1"); // restore the exponent/mantissa alignment + emitter.instruction("movq xmm2, rcx"); // php-src compares magnitudes only + emitter.instruction("cmp r10, 5"); // mode 5 = CEILING + emitter.instruction("je __rt_round_mode_ceiling_x86"); // round toward positive infinity + emitter.instruction("cmp r10, 6"); // mode 6 = FLOOR + emitter.instruction("je __rt_round_mode_floor_x86"); // round toward negative infinity + emitter.instruction("comisd xmm6, xmm2"); // mode 8 = AWAY_FROM_ZERO + emitter.instruction("ja __rt_round_mode_bump_x86"); // any remainder grows the magnitude + emitter.instruction("jmp __rt_round_mode_finish_x86"); // an exact value keeps the integral part + + emitter.label("__rt_round_mode_ceiling_x86"); + emitter.instruction("xorpd xmm5, xmm5"); // xmm5 = 0.0 for the sign test + emitter.instruction("comisd xmm0, xmm5"); // CEILING only moves strictly positive values + emitter.instruction("jbe __rt_round_mode_finish_x86"); // non-positive values already sit at the ceiling + emitter.instruction("comisd xmm6, xmm2"); // is there any remainder left to round away? + emitter.instruction("jbe __rt_round_mode_finish_x86"); // an exact value keeps the integral part + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_BITS)); // IEEE-754 payload of 1.0 + emitter.instruction("movq xmm7, rcx"); // CEILING always adds +1.0, never copysign() + emitter.instruction("jmp __rt_round_mode_bump_x86"); // step toward positive infinity + + emitter.label("__rt_round_mode_floor_x86"); + emitter.instruction("xorpd xmm5, xmm5"); // xmm5 = 0.0 for the sign test + emitter.instruction("comisd xmm5, xmm0"); // FLOOR only moves strictly negative values + emitter.instruction("jbe __rt_round_mode_finish_x86"); // non-negative values already sit at the floor + emitter.instruction("comisd xmm6, xmm2"); // is there any remainder left to round away? + emitter.instruction("jbe __rt_round_mode_finish_x86"); // an exact value keeps the integral part + emitter.instruction(&format!("mov rcx, 0x{:x}", ONE_BITS | (1u64 << 63))); // IEEE-754 payload of -1.0 + emitter.instruction("movq xmm7, rcx"); // FLOOR always subtracts 1.0, never copysign() + + emitter.label("__rt_round_mode_bump_x86"); + emitter.instruction("addsd xmm3, xmm7"); // move the integral part one step in the chosen direction + + // -- unscale the rounded integral part back to the requested precision -- + emitter.label("__rt_round_mode_finish_x86"); + emitter.instruction("xorpd xmm5, xmm5"); // xmm5 = 0.0 for the zero test + emitter.instruction("ucomisd xmm3, xmm5"); // a zero integral part already carries the final sign + emitter.instruction("jp __rt_round_mode_finish_scale_x86"); // an unordered compare cannot be a zero + emitter.instruction("je __rt_round_mode_return_integral_x86"); // avoid 0.0 * INF turning an absurd precision into NAN + emitter.label("__rt_round_mode_finish_scale_x86"); + emitter.instruction("cmp r9, 0"); // unscale the result the way php-src does + emitter.instruction("jg __rt_round_mode_result_div_x86"); // positive precision unscales by dividing + emitter.instruction("mulsd xmm3, xmm1"); // unscale the rounded value by multiplying + emitter.instruction("movsd xmm0, xmm3"); // move the rounded value into the result register + emitter.instruction("jmp __rt_round_mode_return_x86"); // the rounded result is ready + emitter.label("__rt_round_mode_result_div_x86"); + emitter.instruction("divsd xmm3, xmm1"); // unscale the rounded value by dividing + emitter.instruction("movsd xmm0, xmm3"); // move the rounded value into the result register + emitter.instruction("jmp __rt_round_mode_return_x86"); // the rounded result is ready + + emitter.label("__rt_round_mode_return_integral_x86"); + emitter.instruction("movsd xmm0, xmm3"); // return the signed zero unchanged + emitter.instruction("jmp __rt_round_mode_return_x86"); // fall through to the shared epilogue + + emitter.label("__rt_round_mode_return_value_x86"); + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // PHP returns the untouched $num for these inputs + + emitter.label("__rt_round_mode_return_x86"); + emitter.instruction("add rsp, 64"); // release the round-mode spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return with xmm0 = rounded value +} diff --git a/src/codegen_support/runtime/strings/addslashes.rs b/src/codegen_support/runtime/strings/addslashes.rs index 381bba2c79..bac86d49f3 100644 --- a/src/codegen_support/runtime/strings/addslashes.rs +++ b/src/codegen_support/runtime/strings/addslashes.rs @@ -7,10 +7,12 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - The worst-case `2 * len` escaped result is reserved through `__rt_concat_reserve` before +//! the first store, so long inputs fall back to heap storage instead of running off the end +//! of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `__rt_addslashes` runtime helper for PHP's `addslashes()`. /// @@ -21,14 +23,16 @@ use crate::codegen_support::abi; /// ## ARM64 ABI (default) /// - Input: `x1` = source string pointer, `x2` = source string length /// - Output: `x1` = result string pointer, `x2` = result string length -/// - Uses the concat buffer (`_concat_buf` / `_concat_off`) for output storage -/// - Clobbers: `x8`-`x13` +/// - Reserves the worst-case `2 * len` expansion through `__rt_concat_reserve` (concat scratch +/// while it fits, owned heap storage otherwise) and finishes through `__rt_concat_publish`. /// /// ## x86_64 Linux ABI /// - Input: `rax` = source string pointer, `rdx` = source string length /// - Output: `rax` = result string pointer, `rdx` = result string length -/// - Uses the concat buffer (`_concat_buf` / `_concat_off`) for output storage -/// - Clobbers: `r8`-`r11`, `rcx` +/// - Same reservation contract as the ARM64 path. +/// +/// Both paths clobber every caller-saved register, because the reservation can reach +/// `__rt_heap_alloc`, and a wrapped `2 * len` product reports PHP's allocation-overflow fatal. pub fn emit_addslashes(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_addslashes_linux_x86_64(emitter); @@ -39,12 +43,17 @@ pub fn emit_addslashes(emitter: &mut Emitter) { emitter.comment("--- runtime: addslashes ---"); emitter.label_global("__rt_addslashes"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case two-bytes-per-input-byte escaped result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the addslashes helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("adds x0, x2, x2"); // compute the worst-case escaped result size and record unsigned wrap + emitter.instruction("b.cs __rt_addslashes_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_addslashes_loop"); @@ -71,26 +80,40 @@ pub fn emit_addslashes(emitter: &mut Emitter) { emitter.label("__rt_addslashes_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the addslashes helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_addslashes_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of `__rt_addslashes`. /// /// Identical behavior to the ARM64 variant but uses x86_64 System V ABI /// registers: `rax`/`rdx` for pointer/length, `r8`-`r11` and `rcx` as temporaries. +/// Reserves the worst-case `2 * len` expansion through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`. fn emit_addslashes_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: addslashes ---"); emitter.label_global("__rt_addslashes"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer absolute offset before appending the escaped string - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // materialize the concat-buffer base pointer for the escaped string write - emitter.instruction("add r9, r8"); // compute the current concat-buffer write pointer from the base plus offset + // -- reserve the worst-case two-bytes-per-input-byte escaped result before writing anything -- + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("imul rax, rdx, 2"); // compute the worst-case escaped result size as 2 * source length + emitter.instruction("jo __rt_addslashes_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov r9, rax"); // compute the destination write pointer where the escaped string begins emitter.instruction("mov r10, r9"); // preserve the escaped-string start pointer for the final result slice - emitter.instruction("mov rcx, rdx"); // track how many source bytes remain to be escaped + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // track how many source bytes remain to be escaped + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the borrowed source cursor the escape loop advances through emitter.label("__rt_addslashes_loop"); emitter.instruction("test rcx, rcx"); // have we consumed every byte of the source string? @@ -116,10 +139,14 @@ fn emit_addslashes_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_addslashes_done"); emitter.instruction("mov rax, r10"); // return the escaped-string start pointer in the x86_64 string result pointer register - emitter.instruction("mov rdx, r9"); // snapshot the final concat-buffer write pointer before computing the escaped result length + emitter.instruction("mov rdx, r9"); // snapshot the final destination write pointer before computing the escaped result length emitter.instruction("sub rdx, r10"); // compute the escaped result length from the write pointer minus the start pointer - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the previous concat-buffer absolute offset before publishing the appended slice - emitter.instruction("add r8, rdx"); // advance the concat-buffer absolute offset by the escaped result length - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated concat-buffer absolute offset for later writers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the addslashes spill slots before returning the escaped string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the escaped string emitter.instruction("ret"); // return to the caller with the escaped string slice in rax/rdx + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_addslashes_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/base64_decode.rs b/src/codegen_support/runtime/strings/base64_decode.rs index 7b6c7cabeb..563026c325 100644 --- a/src/codegen_support/runtime/strings/base64_decode.rs +++ b/src/codegen_support/runtime/strings/base64_decode.rs @@ -1,23 +1,55 @@ //! Purpose: -//! Emits the `__rt_base64_decode`, `__rt_b64dec_loop` runtime helper assembly for base64 decode. -//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! Emits the `__rt_base64_decode` runtime helper assembly, a byte-for-byte port of php-src's +//! `php_base64_decode_impl` including its `$strict` mode. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: -//! - Base64 helpers depend on fixed encode/decode tables and must report decoded pointer/length pairs consistently. +//! - The decoder is a SINGLE-CHARACTER state machine driven by an `i % 4` accumulator, not a +//! four-characters-at-a-time chunk loop. That distinction is the whole bug fix: a skipped +//! byte (whitespace, or any stray byte in the lax mode) must not shift the remaining input +//! into the wrong quartet lane, and a missing final `=` must still flush the bytes already +//! accumulated. The old chunked loop got `base64_decode("SGVs bG8=")`, `"SGVsbG8"`, and +//! `"SGVsbG8*"` all wrong. +//! - `_b64_decode_tbl` classifies every byte in one load: `0..=63` is a sextet, +//! `B64_DECODE_SKIP` is php-src's `-1` (whitespace, dropped in both modes), and +//! `B64_DECODE_INVALID` is its `-2` (dropped in the lax mode, `false` in strict mode). +//! `=` is recognized before the lookup, exactly as php-src does. +//! - Result storage comes from `__rt_concat_reserve`/`__rt_concat_publish`, so an input above +//! the 64 KiB scratch capacity is served from an owned heap block. A strict rejection +//! releases that reservation through `__rt_heap_free_safe` (a no-op for scratch pointers) +//! instead of leaking it. -use crate::codegen_support::{emit::Emitter, platform::Arch}; use crate::codegen_support::abi; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Reverse-table sentinel for php-src's `-1`: a byte that is skipped in BOTH decode modes. +/// +/// php-src assigns it to exactly the five whitespace bytes listed in +/// [`B64_DECODE_WHITESPACE`]; every other non-alphabet byte is [`B64_DECODE_INVALID`]. +pub const B64_DECODE_SKIP: u8 = 0xFE; + +/// Reverse-table sentinel for php-src's `-2`: a byte outside the Base64 alphabet. +/// +/// The lax mode drops it and keeps decoding; `$strict = true` returns `false` on the first +/// one. `=` also carries this value, but the decoder tests for it before the table load +/// because padding has its own accounting. +pub const B64_DECODE_INVALID: u8 = 0xFF; + +/// The exact byte set php-src marks skippable in `base64_reverse_table`. +/// +/// Tab, line feed, form feed, carriage return, and space — deliberately NOT vertical tab +/// (`0x0B`), which php-src rejects and `u8::is_ascii_whitespace` would have accepted. +pub const B64_DECODE_WHITESPACE: &[u8] = &[b'\t', b'\n', 0x0C, b'\r', b' ']; /// Emits the `__rt_base64_decode` runtime helper. /// -/// ABI (ARM64): x0=input ptr, x2=input byte length; returns x1=result ptr, x2=result length. -/// Output is appended to the shared `_concat_buf` / `_concat_off` concat buffer. -/// Uses `_b64_decode_tbl` for the base64→byte reverse lookup table. -/// Handles `=` padding: `==` produces 1 output byte, `=` produces 2 output bytes. -/// Dispatches to `emit_base64_decode_linux_x86_64` on x86_64; uses inline ARM64 otherwise. +/// ABI (AArch64): `x1` = encoded pointer, `x2` = encoded byte length, `x3` = `$strict` flag. +/// Returns `x1`/`x2` = decoded pointer/length and `x0` = 1 on success, 0 when strict mode +/// rejected the input (in which case `x1`/`x2` are a null/empty pair). +/// +/// Dispatches to `emit_base64_decode_linux_x86_64` on x86_64; uses inline AArch64 otherwise. pub fn emit_base64_decode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_base64_decode_linux_x86_64(emitter); @@ -28,192 +60,268 @@ pub fn emit_base64_decode(emitter: &mut Emitter) { emitter.comment("--- runtime: base64_decode ---"); emitter.label_global("__rt_base64_decode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start - emitter.instruction("mov x11, x2"); // remaining byte count + // -- reserve the decoded result up front: 3 bytes out per 4 in, so the encoded length is + // always an upper bound, and floor(3n/4) <= n-1 keeps every partial-byte store in range -- + emitter.instruction("sub sp, sp, #48"); // allocate spill space for the borrowed encoded string and the strict flag + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #32"); // establish the base64 decoder frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the encoded pointer and length across the reservation call + emitter.instruction("str x3, [sp, #16]"); // save the $strict flag across the reservation call + emitter.instruction("mov x0, x2"); // the decoded payload never exceeds the encoded character count + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov x9, x0"); // keep the reservation start as the decoded string base + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed encoded pointer and length + emitter.instruction("ldr x3, [sp, #16]"); // reload the $strict flag for the per-character decisions + emitter.instruction("mov x10, #0"); // j: index of the decoded byte currently being assembled + emitter.instruction("mov x11, #0"); // i: count of ACCEPTED characters, the `i % 4` quartet lane + emitter.instruction("mov x12, #0"); // padding: '=' characters seen since the last accepted character + abi::emit_symbol_address(emitter, "x15", "_b64_decode_tbl"); - // -- load base64 decode lookup table -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x15", "_b64_decode_tbl"); - - // -- process 4 chars at a time -- + // -- one character per iteration: skipped bytes must not rotate the quartet lane -- emitter.label("__rt_b64dec_loop"); - emitter.instruction("cmp x11, #4"); // at least 4 chars left? - emitter.instruction("b.lt __rt_b64dec_done"); // no -> done - - // -- load and decode 4 base64 chars -- - emitter.instruction("ldrb w12, [x1], #1"); // load char 0 - emitter.instruction("ldrb w12, [x15, x12]"); // decode char 0 via table - emitter.instruction("ldrb w13, [x1], #1"); // load char 1 - emitter.instruction("ldrb w13, [x15, x13]"); // decode char 1 via table - emitter.instruction("ldrb w14, [x1], #1"); // load char 2 - emitter.instruction("ldrb w16, [x1], #1"); // load char 3 - emitter.instruction("sub x11, x11, #4"); // consumed 4 chars - - // -- check for '=' padding in char 2 -- - emitter.instruction("cmp w14, #61"); // is char 2 '='? - emitter.instruction("b.eq __rt_b64dec_pad2"); // yes -> only 1 output byte - - // -- decode char 2 via table -- - emitter.instruction("ldrb w14, [x15, x14]"); // decode char 2 - - // -- check for '=' padding in char 3 -- - emitter.instruction("cmp w16, #61"); // is char 3 '='? - emitter.instruction("b.eq __rt_b64dec_pad1"); // yes -> only 2 output bytes - - // -- decode char 3 via table -- - emitter.instruction("ldrb w16, [x15, x16]"); // decode char 3 - - // -- output byte 0: (val0 << 2) | (val1 >> 4) -- - emitter.instruction("lsl w17, w12, #2"); // val0 << 2 - emitter.instruction("lsr w18, w13, #4"); // val1 >> 4 - emitter.instruction("orr w17, w17, w18"); // combine - emitter.instruction("strb w17, [x9], #1"); // write byte 0 - - // -- output byte 1: (val1 << 4) | (val2 >> 2) -- - emitter.instruction("and w17, w13, #0xf"); // val1 & 0xf - emitter.instruction("lsl w17, w17, #4"); // shift left 4 - emitter.instruction("lsr w18, w14, #2"); // val2 >> 2 - emitter.instruction("orr w17, w17, w18"); // combine - emitter.instruction("strb w17, [x9], #1"); // write byte 1 - - // -- output byte 2: (val2 << 6) | val3 -- - emitter.instruction("and w17, w14, #0x3"); // val2 & 0x3 - emitter.instruction("lsl w17, w17, #6"); // shift left 6 - emitter.instruction("orr w17, w17, w16"); // combine with val3 - emitter.instruction("strb w17, [x9], #1"); // write byte 2 - emitter.instruction("b __rt_b64dec_loop"); // next 4 chars - - // -- padding: char2 is '=', only 1 output byte -- - emitter.label("__rt_b64dec_pad2"); - emitter.instruction("lsl w17, w12, #2"); // val0 << 2 - emitter.instruction("lsr w18, w13, #4"); // val1 >> 4 - emitter.instruction("orr w17, w17, w18"); // combine - emitter.instruction("strb w17, [x9], #1"); // write byte 0 - emitter.instruction("b __rt_b64dec_done"); // done (skip rest) - - // -- padding: char3 is '=', only 2 output bytes -- - emitter.label("__rt_b64dec_pad1"); - // output byte 0 - emitter.instruction("lsl w17, w12, #2"); // val0 << 2 - emitter.instruction("lsr w18, w13, #4"); // val1 >> 4 - emitter.instruction("orr w17, w17, w18"); // combine - emitter.instruction("strb w17, [x9], #1"); // write byte 0 - // output byte 1 - emitter.instruction("and w17, w13, #0xf"); // val1 & 0xf - emitter.instruction("lsl w17, w17, #4"); // shift left 4 - emitter.instruction("lsr w18, w14, #2"); // val2 >> 2 - emitter.instruction("orr w17, w17, w18"); // combine - emitter.instruction("strb w17, [x9], #1"); // write byte 1 - emitter.instruction("b __rt_b64dec_done"); // done (skip rest) + emitter.instruction("cbz x2, __rt_b64dec_end"); // stop once every encoded byte has been classified + emitter.instruction("ldrb w13, [x1], #1"); // load the next encoded byte and advance the cursor + emitter.instruction("sub x2, x2, #1"); // record that one encoded byte has been consumed + emitter.instruction("cmp w13, #61"); // is this byte '=' padding? + emitter.instruction("b.eq __rt_b64dec_pad"); // padding is counted, never decoded + emitter.instruction("ldrb w13, [x15, x13]"); // classify the byte through the php-src reverse table + emitter.instruction(&format!("cmp w13, #{}", B64_DECODE_SKIP)); // is this one of php-src's five skippable whitespace bytes? + emitter.instruction("b.eq __rt_b64dec_loop"); // whitespace is dropped in both decode modes + emitter.instruction(&format!("cmp w13, #{}", B64_DECODE_INVALID)); // is this byte outside the Base64 alphabet? + emitter.instruction("b.eq __rt_b64dec_invalid"); // strict mode rejects it; the lax mode drops it + + // -- an accepted character after padding ends the message in strict mode -- + emitter.instruction("cbz x12, __rt_b64dec_accept"); // no padding seen, so the character is accepted directly + emitter.instruction("cbnz x3, __rt_b64dec_fail"); // strict mode forbids data after a padding character + emitter.instruction("mov x12, #0"); // the lax mode forgets the padding and keeps decoding + + // -- dispatch on the quartet lane, mirroring php-src's `switch (i % 4)` -- + emitter.label("__rt_b64dec_accept"); + emitter.instruction("and x14, x11, #3"); // compute the quartet lane of this accepted character + emitter.instruction("cbz x14, __rt_b64dec_case0"); // lane 0 starts a new decoded byte + emitter.instruction("cmp x14, #1"); // is this the second character of the quartet? + emitter.instruction("b.eq __rt_b64dec_case1"); // lane 1 finishes byte 0 and opens byte 1 + emitter.instruction("cmp x14, #2"); // is this the third character of the quartet? + emitter.instruction("b.eq __rt_b64dec_case2"); // lane 2 finishes byte 1 and opens byte 2 + + // -- lane 3: the low six bits complete the third decoded byte -- + emitter.instruction("ldrb w16, [x9, x10]"); // reload the decoded byte opened by lane 2 + emitter.instruction("orr w16, w16, w13"); // fold in all six bits of this sextet + emitter.instruction("strb w16, [x9, x10]"); // publish the completed decoded byte + emitter.instruction("add x10, x10, #1"); // the quartet produced its third and final byte + emitter.instruction("b __rt_b64dec_next"); // count the accepted character and continue + + // -- lane 0: the sextet becomes the top six bits of a fresh decoded byte -- + emitter.label("__rt_b64dec_case0"); + emitter.instruction("lsl w16, w13, #2"); // shift the sextet into the high bits of the new byte + emitter.instruction("strb w16, [x9, x10]"); // open the decoded byte without committing it yet + emitter.instruction("b __rt_b64dec_next"); // count the accepted character and continue + + // -- lane 1: two bits finish byte 0, four bits open byte 1 -- + emitter.label("__rt_b64dec_case1"); + emitter.instruction("ldrb w16, [x9, x10]"); // reload the decoded byte opened by lane 0 + emitter.instruction("lsr w17, w13, #4"); // take the top two bits of this sextet + emitter.instruction("orr w16, w16, w17"); // complete the first decoded byte of the quartet + emitter.instruction("strb w16, [x9, x10]"); // publish the completed decoded byte + emitter.instruction("add x10, x10, #1"); // move to the next decoded byte position + emitter.instruction("and w17, w13, #0xf"); // keep the low four bits of this sextet + emitter.instruction("lsl w17, w17, #4"); // shift them into the high bits of the next byte + emitter.instruction("strb w17, [x9, x10]"); // open the second decoded byte of the quartet + emitter.instruction("b __rt_b64dec_next"); // count the accepted character and continue + + // -- lane 2: four bits finish byte 1, two bits open byte 2 -- + emitter.label("__rt_b64dec_case2"); + emitter.instruction("ldrb w16, [x9, x10]"); // reload the decoded byte opened by lane 1 + emitter.instruction("lsr w17, w13, #2"); // take the top four bits of this sextet + emitter.instruction("orr w16, w16, w17"); // complete the second decoded byte of the quartet + emitter.instruction("strb w16, [x9, x10]"); // publish the completed decoded byte + emitter.instruction("add x10, x10, #1"); // move to the next decoded byte position + emitter.instruction("and w17, w13, #0x3"); // keep the low two bits of this sextet + emitter.instruction("lsl w17, w17, #6"); // shift them into the high bits of the next byte + emitter.instruction("strb w17, [x9, x10]"); // open the third decoded byte of the quartet + + emitter.label("__rt_b64dec_next"); + emitter.instruction("add x11, x11, #1"); // one more accepted character advances the quartet lane + emitter.instruction("b __rt_b64dec_loop"); // classify the next encoded byte + + // -- '=' only ever increments the padding tally -- + emitter.label("__rt_b64dec_pad"); + emitter.instruction("add x12, x12, #1"); // count this padding character for the strict-mode checks + emitter.instruction("b __rt_b64dec_loop"); // classify the next encoded byte + + // -- a byte outside the alphabet: rejected in strict mode, dropped otherwise -- + emitter.label("__rt_b64dec_invalid"); + emitter.instruction("cbnz x3, __rt_b64dec_fail"); // strict mode returns false on the first stray byte + emitter.instruction("b __rt_b64dec_loop"); // the lax mode ignores it and keeps decoding + + // -- strict-mode end-of-input validation, in php-src's order -- + emitter.label("__rt_b64dec_end"); + emitter.instruction("cbz x3, __rt_b64dec_done"); // the lax mode accepts whatever was decoded + emitter.instruction("and x14, x11, #3"); // recover the quartet lane the input ended on + emitter.instruction("cmp x14, #1"); // did the final group hold a single character? + emitter.instruction("b.eq __rt_b64dec_fail"); // one leftover character cannot encode any byte + emitter.instruction("cbz x12, __rt_b64dec_done"); // unpadded input is accepted when the group is not truncated + emitter.instruction("cmp x12, #2"); // more than two padding characters is never valid + emitter.instruction("b.gt __rt_b64dec_fail"); // reject over-padded input such as "A===" + emitter.instruction("add x14, x11, x12"); // characters plus padding must complete whole quartets + emitter.instruction("and x14, x14, #3"); // check that combined count against the quartet size + emitter.instruction("cbnz x14, __rt_b64dec_fail"); // reject misplaced padding such as "SGVsbG8==" emitter.label("__rt_b64dec_done"); - emitter.instruction("mov x1, x10"); // result pointer - emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset - emitter.instruction("ret"); // return + emitter.instruction("mov x1, x9"); // return the decoded payload pointer + emitter.instruction("mov x2, x10"); // return the number of decoded bytes actually written + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("mov x0, #1"); // report a successful decode to the caller + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the base64 decoder frame + emitter.instruction("ret"); // return the decoded string pair + + // -- strict rejection: release the reservation and report PHP's `false` -- + emitter.label("__rt_b64dec_fail"); + emitter.instruction("mov x0, x9"); // release the reservation that will never be published + emitter.instruction("bl __rt_heap_free_safe"); // free an oversized heap reservation; scratch pointers are skipped + emitter.instruction("mov x0, #0"); // report the strict-mode rejection to the caller + emitter.instruction("mov x1, #0"); // hand back a null payload pointer with the failure + emitter.instruction("mov x2, #0"); // hand back a zero payload length with the failure + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the base64 decoder frame + emitter.instruction("ret"); // return PHP's `false` decode result } /// Emits the `__rt_base64_decode` runtime helper for the Linux x86_64 target. /// -/// ABI (x86_64): rax=input ptr, rdx=input byte count; returns rax=result ptr, rdx=result length. -/// Output is appended to the shared `_concat_buf` / `_concat_off` concat buffer. -/// Uses `_b64_decode_tbl` for the base64→byte reverse lookup table. -/// Handles `=` padding: `==` produces 1 output byte, `=` produces 2 output bytes. +/// ABI (x86_64): `rax` = encoded pointer, `rdx` = encoded byte length, `rdi` = `$strict` flag. +/// Returns `rax`/`rdx` = decoded pointer/length and `r8` = 1 on success, 0 when strict mode +/// rejected the input. +/// +/// Same single-character state machine as the AArch64 path; the padding tally lives in a +/// frame slot because the alphabet table, the input cursor, the output cursor, the accepted +/// count, and the strict flag already occupy the free caller-saved registers. /// Called exclusively from `emit_base64_decode` when `emitter.target.arch == Arch::X86_64`. fn emit_base64_decode_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: base64_decode ---"); emitter.label_global("__rt_base64_decode"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer offset before appending the decoded bytes - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // load the base address of the shared concat buffer - emitter.instruction("add r9, r8"); // compute the destination pointer at the current concat-buffer tail - emitter.instruction("mov r10, r9"); // preserve the decoded string start pointer for the return value - emitter.instruction("mov rcx, rdx"); // copy the encoded character count into a decrementing loop counter - emitter.instruction("mov rsi, rax"); // copy the encoded string pointer into a cursor register for byte-by-byte reads - emitter.label("__rt_b64dec_loop_linux_x86_64"); - abi::emit_symbol_address(emitter, "r11", "_b64_decode_tbl"); // reload the base64 reverse-lookup table address for this decoding iteration - emitter.instruction("cmp rcx, 4"); // check whether at least one full 4-character chunk remains - emitter.instruction("jl __rt_b64dec_done_linux_x86_64"); // stop once fewer than 4 encoded characters remain - - emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load encoded char 0 and widen it for table lookup - emitter.instruction("add rsi, 1"); // advance the encoded-string cursor past char 0 - emitter.instruction("movzx eax, BYTE PTR [r11 + rax]"); // decode char 0 through the reverse lookup table - emitter.instruction("movzx edx, BYTE PTR [rsi]"); // load encoded char 1 and widen it for table lookup - emitter.instruction("add rsi, 1"); // advance the encoded-string cursor past char 1 - emitter.instruction("movzx edx, BYTE PTR [r11 + rdx]"); // decode char 1 through the reverse lookup table - emitter.instruction("movzx r8d, BYTE PTR [rsi]"); // load encoded char 2 so padding can be checked before table lookup - emitter.instruction("add rsi, 1"); // advance the encoded-string cursor past char 2 - emitter.instruction("movzx edi, BYTE PTR [rsi]"); // load encoded char 3 so padding can be checked before table lookup - emitter.instruction("add rsi, 1"); // advance the encoded-string cursor past char 3 - emitter.instruction("sub rcx, 4"); // record that one full 4-character chunk has been consumed - - emitter.instruction("cmp r8d, 61"); // check whether encoded char 2 is '=' padding - emitter.instruction("je __rt_b64dec_pad2_linux_x86_64"); // branch when only one decoded output byte remains - emitter.instruction("movzx r8d, BYTE PTR [r11 + r8]"); // decode char 2 through the reverse lookup table - emitter.instruction("cmp edi, 61"); // check whether encoded char 3 is '=' padding - emitter.instruction("je __rt_b64dec_pad1_linux_x86_64"); // branch when only two decoded output bytes remain - emitter.instruction("movzx edi, BYTE PTR [r11 + rdi]"); // decode char 3 through the reverse lookup table - - emitter.instruction("shl eax, 2"); // move decoded value 0 into the output-byte 0 position - emitter.instruction("mov r11d, edx"); // copy decoded value 1 into a scratch register for output byte 0 assembly - emitter.instruction("shr r11d, 4"); // keep the upper 2 decoded bits from value 1 for output byte 0 - emitter.instruction("or eax, r11d"); // combine the carried decoded bits into output byte 0 - emitter.instruction("mov BYTE PTR [r9], al"); // write decoded output byte 0 to the destination buffer - emitter.instruction("add r9, 1"); // advance the destination cursor after writing output byte 0 - - emitter.instruction("and edx, 15"); // keep the low 4 decoded bits from value 1 for output byte 1 - emitter.instruction("shl edx, 4"); // move those 4 bits into their output-byte position - emitter.instruction("mov r11d, r8d"); // copy decoded value 2 into a scratch register for its upper bits - emitter.instruction("shr r11d, 2"); // keep the upper 4 decoded bits from value 2 for output byte 1 - emitter.instruction("or edx, r11d"); // combine the carried decoded bits into output byte 1 - emitter.instruction("mov BYTE PTR [r9], dl"); // write decoded output byte 1 to the destination buffer - emitter.instruction("add r9, 1"); // advance the destination cursor after writing output byte 1 - - emitter.instruction("and r8d, 3"); // keep the low 2 decoded bits from value 2 for output byte 2 - emitter.instruction("shl r8d, 6"); // move those 2 bits into their output-byte position - emitter.instruction("or r8d, edi"); // combine the carried decoded bits with decoded value 3 - emitter.instruction("mov BYTE PTR [r9], r8b"); // write decoded output byte 2 to the destination buffer - emitter.instruction("add r9, 1"); // advance the destination cursor after writing output byte 2 - emitter.instruction("jmp __rt_b64dec_loop_linux_x86_64"); // continue decoding subsequent 4-character chunks - - emitter.label("__rt_b64dec_pad2_linux_x86_64"); - emitter.instruction("shl eax, 2"); // move decoded value 0 into the output-byte position for the '==' padded chunk - emitter.instruction("mov r11d, edx"); // copy decoded value 1 into a scratch register for the padded output byte - emitter.instruction("shr r11d, 4"); // keep the upper 2 decoded bits from value 1 for the padded output byte - emitter.instruction("or eax, r11d"); // combine the carried decoded bits into the single padded output byte - emitter.instruction("mov BYTE PTR [r9], al"); // write the single decoded output byte for the '==' padded chunk - emitter.instruction("add r9, 1"); // advance the destination cursor after writing the single padded output byte - emitter.instruction("jmp __rt_b64dec_done_linux_x86_64"); // finish after the '==' padded chunk - - emitter.label("__rt_b64dec_pad1_linux_x86_64"); - emitter.instruction("shl eax, 2"); // move decoded value 0 into the output-byte 0 position for the '=' padded chunk - emitter.instruction("mov r11d, edx"); // copy decoded value 1 into a scratch register for output byte 0 assembly - emitter.instruction("shr r11d, 4"); // keep the upper 2 decoded bits from value 1 for output byte 0 - emitter.instruction("or eax, r11d"); // combine the carried decoded bits into output byte 0 - emitter.instruction("mov BYTE PTR [r9], al"); // write decoded output byte 0 to the destination buffer - emitter.instruction("add r9, 1"); // advance the destination cursor after writing output byte 0 - emitter.instruction("and edx, 15"); // keep the low 4 decoded bits from value 1 for output byte 1 - emitter.instruction("shl edx, 4"); // move those 4 bits into their output-byte position - emitter.instruction("mov r11d, r8d"); // copy decoded value 2 into a scratch register for its upper bits - emitter.instruction("shr r11d, 2"); // keep the upper 4 decoded bits from value 2 for output byte 1 - emitter.instruction("or edx, r11d"); // combine the carried decoded bits into output byte 1 - emitter.instruction("mov BYTE PTR [r9], dl"); // write decoded output byte 1 to the destination buffer - emitter.instruction("add r9, 1"); // advance the destination cursor after writing output byte 1 - - emitter.label("__rt_b64dec_done_linux_x86_64"); - emitter.instruction("mov rax, r10"); // return the decoded string start pointer in the standard x86_64 string result register - emitter.instruction("mov rdx, r9"); // copy the concat-buffer tail into the length scratch register - emitter.instruction("sub rdx, r10"); // compute the decoded string length from the written byte count - emitter.instruction("mov r8, r9"); // copy the absolute concat-buffer tail before normalizing it back to a shared offset - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // load the concat-buffer base so the shared offset can stay relative - emitter.instruction("sub r8, r11"); // convert the absolute concat-buffer tail back into the shared relative offset - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated relative concat-buffer offset for later string appenders - emitter.instruction("ret"); // return the decoded string through the standard x86_64 string result registers -} \ No newline at end of file + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed encoded string + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the input, the strict flag, and the padding tally + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the encoded string pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the encoded character count across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the $strict flag across the reservation call + emitter.instruction("mov rax, rdx"); // the decoded payload never exceeds the encoded character count + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov r9, rax"); // keep the reservation start as the decoded string base + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // padding: '=' characters seen since the last accepted character + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload the encoded string pointer into the read cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the encoded character count into the loop counter + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the $strict flag for the per-character decisions + emitter.instruction("xor r10d, r10d"); // j: index of the decoded byte currently being assembled + emitter.instruction("xor r11d, r11d"); // i: count of ACCEPTED characters, the `i % 4` quartet lane + abi::emit_symbol_address(emitter, "r8", "_b64_decode_tbl"); // hold the php-src reverse-lookup table for the whole decode loop + + emitter.label("__rt_b64dec_loop_x86"); + emitter.instruction("test rcx, rcx"); // stop once every encoded byte has been classified + emitter.instruction("jz __rt_b64dec_end_x86"); // leave the loop at the end of the encoded input + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the next encoded byte and widen it for the table lookup + emitter.instruction("add rsi, 1"); // advance the encoded-string read cursor + emitter.instruction("sub rcx, 1"); // record that one encoded byte has been consumed + emitter.instruction("cmp eax, 61"); // is this byte '=' padding? + emitter.instruction("je __rt_b64dec_pad_x86"); // padding is counted, never decoded + emitter.instruction("movzx eax, BYTE PTR [r8 + rax]"); // classify the byte through the php-src reverse table + emitter.instruction(&format!("cmp eax, {}", B64_DECODE_SKIP)); // is this one of php-src's five skippable whitespace bytes? + emitter.instruction("je __rt_b64dec_loop_x86"); // whitespace is dropped in both decode modes + emitter.instruction(&format!("cmp eax, {}", B64_DECODE_INVALID)); // is this byte outside the Base64 alphabet? + emitter.instruction("je __rt_b64dec_invalid_x86"); // strict mode rejects it; the lax mode drops it + + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // has any padding character been seen already? + emitter.instruction("je __rt_b64dec_accept_x86"); // no padding seen, so the character is accepted directly + emitter.instruction("test rdi, rdi"); // is this a strict decode? + emitter.instruction("jnz __rt_b64dec_fail_x86"); // strict mode forbids data after a padding character + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // the lax mode forgets the padding and keeps decoding + + emitter.label("__rt_b64dec_accept_x86"); + emitter.instruction("mov rdx, r11"); // copy the accepted-character count before reducing it + emitter.instruction("and rdx, 3"); // compute the quartet lane of this accepted character + emitter.instruction("jz __rt_b64dec_case0_x86"); // lane 0 starts a new decoded byte + emitter.instruction("cmp rdx, 1"); // is this the second character of the quartet? + emitter.instruction("je __rt_b64dec_case1_x86"); // lane 1 finishes byte 0 and opens byte 1 + emitter.instruction("cmp rdx, 2"); // is this the third character of the quartet? + emitter.instruction("je __rt_b64dec_case2_x86"); // lane 2 finishes byte 1 and opens byte 2 + + emitter.instruction("or BYTE PTR [r9 + r10], al"); // fold all six bits of this sextet into the byte opened by lane 2 + emitter.instruction("add r10, 1"); // the quartet produced its third and final byte + emitter.instruction("jmp __rt_b64dec_next_x86"); // count the accepted character and continue + + emitter.label("__rt_b64dec_case0_x86"); + emitter.instruction("shl eax, 2"); // shift the sextet into the high bits of the new byte + emitter.instruction("mov BYTE PTR [r9 + r10], al"); // open the decoded byte without committing it yet + emitter.instruction("jmp __rt_b64dec_next_x86"); // count the accepted character and continue + + emitter.label("__rt_b64dec_case1_x86"); + emitter.instruction("mov edx, eax"); // copy the sextet before splitting it across two decoded bytes + emitter.instruction("shr edx, 4"); // take the top two bits of this sextet + emitter.instruction("or BYTE PTR [r9 + r10], dl"); // complete the first decoded byte of the quartet + emitter.instruction("add r10, 1"); // move to the next decoded byte position + emitter.instruction("and eax, 15"); // keep the low four bits of this sextet + emitter.instruction("shl eax, 4"); // shift them into the high bits of the next byte + emitter.instruction("mov BYTE PTR [r9 + r10], al"); // open the second decoded byte of the quartet + emitter.instruction("jmp __rt_b64dec_next_x86"); // count the accepted character and continue + + emitter.label("__rt_b64dec_case2_x86"); + emitter.instruction("mov edx, eax"); // copy the sextet before splitting it across two decoded bytes + emitter.instruction("shr edx, 2"); // take the top four bits of this sextet + emitter.instruction("or BYTE PTR [r9 + r10], dl"); // complete the second decoded byte of the quartet + emitter.instruction("add r10, 1"); // move to the next decoded byte position + emitter.instruction("and eax, 3"); // keep the low two bits of this sextet + emitter.instruction("shl eax, 6"); // shift them into the high bits of the next byte + emitter.instruction("mov BYTE PTR [r9 + r10], al"); // open the third decoded byte of the quartet + + emitter.label("__rt_b64dec_next_x86"); + emitter.instruction("add r11, 1"); // one more accepted character advances the quartet lane + emitter.instruction("jmp __rt_b64dec_loop_x86"); // classify the next encoded byte + + emitter.label("__rt_b64dec_pad_x86"); + emitter.instruction("add QWORD PTR [rbp - 40], 1"); // count this padding character for the strict-mode checks + emitter.instruction("jmp __rt_b64dec_loop_x86"); // classify the next encoded byte + + emitter.label("__rt_b64dec_invalid_x86"); + emitter.instruction("test rdi, rdi"); // is this a strict decode? + emitter.instruction("jnz __rt_b64dec_fail_x86"); // strict mode returns false on the first stray byte + emitter.instruction("jmp __rt_b64dec_loop_x86"); // the lax mode ignores it and keeps decoding + + emitter.label("__rt_b64dec_end_x86"); + emitter.instruction("test rdi, rdi"); // is this a strict decode? + emitter.instruction("jz __rt_b64dec_done_x86"); // the lax mode accepts whatever was decoded + emitter.instruction("mov rdx, r11"); // copy the accepted-character count before reducing it + emitter.instruction("and rdx, 3"); // recover the quartet lane the input ended on + emitter.instruction("cmp rdx, 1"); // did the final group hold a single character? + emitter.instruction("je __rt_b64dec_fail_x86"); // one leftover character cannot encode any byte + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // reload the padding tally for the remaining checks + emitter.instruction("test rdx, rdx"); // was the input padded at all? + emitter.instruction("jz __rt_b64dec_done_x86"); // unpadded input is accepted when the group is not truncated + emitter.instruction("cmp rdx, 2"); // more than two padding characters is never valid + emitter.instruction("jg __rt_b64dec_fail_x86"); // reject over-padded input such as "A===" + emitter.instruction("add rdx, r11"); // characters plus padding must complete whole quartets + emitter.instruction("and rdx, 3"); // check that combined count against the quartet size + emitter.instruction("jnz __rt_b64dec_fail_x86"); // reject misplaced padding such as "SGVsbG8==" + + emitter.label("__rt_b64dec_done_x86"); + emitter.instruction("mov rax, r9"); // return the decoded payload pointer + emitter.instruction("mov rdx, r10"); // return the number of decoded bytes actually written + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("mov r8, 1"); // report a successful decode to the caller + emitter.instruction("add rsp, 64"); // release the base64 decoder spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the decoded string pair + + emitter.label("__rt_b64dec_fail_x86"); + emitter.instruction("mov rax, r9"); // release the reservation that will never be published + emitter.instruction("call __rt_heap_free_safe"); // free an oversized heap reservation; scratch pointers are skipped + emitter.instruction("xor eax, eax"); // hand back a null payload pointer with the failure + emitter.instruction("xor edx, edx"); // hand back a zero payload length with the failure + emitter.instruction("xor r8d, r8d"); // report the strict-mode rejection to the caller + emitter.instruction("add rsp, 64"); // release the base64 decoder spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return PHP's `false` decode result +} diff --git a/src/codegen_support/runtime/strings/base64_encode.rs b/src/codegen_support/runtime/strings/base64_encode.rs index 20c2b63c7b..64f44cd8f1 100644 --- a/src/codegen_support/runtime/strings/base64_encode.rs +++ b/src/codegen_support/runtime/strings/base64_encode.rs @@ -37,12 +37,19 @@ pub fn emit_base64_encode(emitter: &mut Emitter) { emitter.comment("--- runtime: base64_encode ---"); emitter.label_global("__rt_base64_encode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case 4-chars-per-3-bytes result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the base64 encoder frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("adds x0, x2, x2"); // start the 4*ceil(len/3) upper bound from 2 * source length + emitter.instruction("b.cs __rt_b64enc_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("adds x0, x0, #4"); // add the padded final quantum so short inputs still fit the bound + emitter.instruction("b.cs __rt_b64enc_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the encoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count // -- load base64 lookup table -- @@ -138,10 +145,14 @@ pub fn emit_base64_encode(emitter: &mut Emitter) { emitter.label("__rt_b64enc_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the base64 encoder frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_b64enc_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of `__rt_base64_encode`. @@ -155,12 +166,21 @@ fn emit_base64_encode_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: base64_encode ---"); emitter.label_global("__rt_base64_encode"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer offset before appending the encoded bytes - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // load the base address of the shared concat buffer - emitter.instruction("add r9, r8"); // compute the destination pointer at the current concat-buffer tail + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the source byte count across the reservation call + emitter.instruction("mov rax, rdx"); // seed the encoded-size bound from the source byte count + emitter.instruction("add rax, rax"); // start the 4*ceil(len/3) upper bound from 2 * source length + emitter.instruction("jc __rt_b64enc_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("add rax, 4"); // add the padded final quantum so short inputs still fit the bound + emitter.instruction("jc __rt_b64enc_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the encoded result + emitter.instruction("mov r9, rax"); // compute the destination pointer at the reserved result start emitter.instruction("mov r10, r9"); // preserve the encoded string start pointer for the return value - emitter.instruction("mov rcx, rdx"); // copy the source byte count into a decrementing loop counter - emitter.instruction("mov rsi, rax"); // copy the source pointer into a cursor register for byte-by-byte reads + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // copy the source byte count into a decrementing loop counter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // copy the source pointer into a cursor register for byte-by-byte reads abi::emit_symbol_address(emitter, "r11", "_b64_encode_tbl"); // load the base64 lookup-table address for the encoding loop emitter.label("__rt_b64enc_loop_linux_x86_64"); @@ -261,12 +281,14 @@ fn emit_base64_encode_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_b64enc_done_linux_x86_64"); emitter.instruction("mov rax, r10"); // return the encoded string start pointer in the standard x86_64 string result register - emitter.instruction("mov rdx, r9"); // copy the concat-buffer tail into the length scratch register + emitter.instruction("mov rdx, r9"); // copy the destination cursor into the length scratch register emitter.instruction("sub rdx, r10"); // compute the encoded string length from the written byte count - abi::emit_store_reg_to_symbol(emitter, "r9", "_concat_off", 0); // temporarily publish the absolute concat-buffer tail before normalizing the shared offset - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the absolute concat-buffer tail through the shared offset slot - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // load the concat-buffer base so the shared offset can stay relative - emitter.instruction("sub r8, r9"); // convert the absolute concat-buffer tail back into the shared relative offset - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated relative concat-buffer offset for later string appenders + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the base64 encoder spill slots before returning the encoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the encoded string emitter.instruction("ret"); // return the encoded string through the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_b64enc_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/base_convert.rs b/src/codegen_support/runtime/strings/base_convert.rs new file mode 100644 index 0000000000..8150073258 --- /dev/null +++ b/src/codegen_support/runtime/strings/base_convert.rs @@ -0,0 +1,302 @@ +//! Purpose: +//! Emits the `__rt_base_convert` runtime helper assembly for PHP's `base_convert`: parses a +//! numeral string in one base and renders it in another, reproducing php-src's float path. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - php-src composes `_php_math_basetozval` and `_php_math_zvaltobase`, so this helper +//! composes the same two runtime pieces: `__rt_base_to_number` for the parse and +//! `__rt_dec_to_base` for the integer render. Both already match reference PHP exactly. +//! - A value past `PHP_INT_MAX` widens to `double` during the parse, and php-src then renders +//! it with a LOSSY loop (`digit = (int) fmod(v, base); v /= base;` without re-flooring). +//! That is why `base_convert("ffffffffffffffff", 16, 10)` is `"18446744073709552046"` and +//! not the exact value; the loop below reproduces those exact float operations. +//! - `fmod` is computed inline by scaled subtraction (double the divisor until it passes the +//! dividend, then halve it back down, subtracting whenever it fits). Every step is exact +//! under IEEE-754, so the result is bit-identical to libc `fmod` without linking libm into +//! the runtime object. +//! - php-src caps the rendered digits at 64 (`char buf[(sizeof(double) << 3) + 1]`) and +//! returns an empty string for an infinite value; both bounds are reproduced here. +//! - Bases outside `2..=36` never reach this helper: the EIR lowering raises php-src's +//! `ValueError` for both base arguments before the call. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Bit pattern of IEEE-754 positive infinity, used to reject an overflowed parse result. +const F64_INFINITY_BITS: i64 = 0x7ff0_0000_0000_0000; + +/// Mask that clears the IEEE-754 sign bit, turning a bit pattern into its magnitude. +const F64_ABS_MASK: i64 = 0x7fff_ffff_ffff_ffff; + +/// Bit pattern of `1.0`, compared as an integer magnitude to test `fabs(value) >= 1`. +const F64_ONE_BITS: i64 = 0x3ff0_0000_0000_0000; + +/// Bit pattern of `0.5`, the exact halving factor of the inline `fmod` reduction. +const F64_HALF_BITS: i64 = 0x3fe0_0000_0000_0000; + +/// Largest digit count php-src's `_php_math_zvaltobase` float buffer can hold. +const MAX_FLOAT_DIGITS: i64 = 64; + +/// Emits the `__rt_base_convert` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = numeral pointer, `x2` = numeral length, `x3` = source base (2..36), +/// `x4` = target base (2..36). +/// Output: `x1` = result pointer, `x2` = result length. +/// +/// ABI (x86_64 System V): +/// Input: `rdi` = numeral pointer, `rsi` = numeral length, `rdx` = source base (2..36), +/// `rcx` = target base (2..36). +/// Output: `rax` = result pointer, `rdx` = result length. +/// +/// Clobbers every caller-saved register. The result is published through +/// `__rt_concat_publish`, so it lives in the shared concat scratch while it fits and in an +/// owned heap block otherwise. +pub fn emit_base_convert(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_base_convert_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: base_convert ---"); + emitter.label_global("__rt_base_convert"); + + emitter.instruction("sub sp, sp, #96"); // reserve the 64-byte digit buffer plus spill and frame slots + emitter.instruction("stp x29, x30, [sp, #80]"); // save the frame pointer and return address across the nested calls + emitter.instruction("add x29, sp, #80"); // establish the base_convert helper frame pointer + emitter.instruction("str x4, [sp, #64]"); // save the target base across the parse call + emitter.instruction("bl __rt_base_to_number"); // parse the numeral exactly like php-src's _php_math_basetozval + emitter.instruction("ldr x4, [sp, #64]"); // reload the target base after the parse + emitter.instruction("cbnz x0, __rt_base_convert_float"); // a widened parse result renders through php-src's lossy float loop + + // -- integer result: render it exactly, like dechex/decbin/decoct do -- + emitter.instruction("mov x0, x1"); // the parsed integer is the value to render + emitter.instruction("mov x3, x4"); // render it in the requested target base + emitter.instruction("bl __rt_dec_to_base"); // reuse the shared unsigned integer-to-base renderer + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the base_convert helper frame + emitter.instruction("ret"); // return the rendered digits as a PHP string pair + + // -- float result: reproduce php-src's floor + fmod digit loop bit for bit -- + emitter.label("__rt_base_convert_float"); + emitter.instruction("frintm d0, d0"); // php-src floors the widened value before rendering it + emitter.instruction("fmov x9, d0"); // inspect the value's bit pattern to reject infinities + abi::emit_load_int_immediate(emitter, "x10", F64_ABS_MASK); + emitter.instruction("and x9, x9, x10"); // drop the sign bit to compare the magnitude + abi::emit_load_int_immediate(emitter, "x10", F64_INFINITY_BITS); + emitter.instruction("cmp x9, x10"); // did the parse overflow to infinity? + emitter.instruction("b.eq __rt_base_convert_too_large"); // php-src returns an empty string for a value it cannot render + emitter.instruction("ucvtf d1, x4"); // keep the target base available as a double divisor + emitter.instruction("add x11, sp, #64"); // point the digit cursor just past the end of the digit buffer + emitter.instruction("mov x12, #0"); // start with no digits emitted + + emitter.label("__rt_base_convert_digit"); + // -- inline exact fmod(d0, d1) into d2 by scaled subtraction -- + emitter.instruction("fmov d2, d0"); // the running remainder starts as the whole value + emitter.instruction("fcmp d2, d1"); // is the value already smaller than the base? + emitter.instruction("b.lt __rt_base_convert_mod_done"); // then it is its own remainder + + emitter.instruction("fmov d5, d1"); // scaled divisor starts at the base itself + emitter.label("__rt_base_convert_scale"); + emitter.instruction("fadd d6, d5, d5"); // double the scaled divisor, exactly + emitter.instruction("fcmp d6, d2"); // has the scaled divisor passed the remainder? + emitter.instruction("b.gt __rt_base_convert_reduce"); // start reducing once doubling would overshoot + emitter.instruction("fmov d5, d6"); // keep the doubled divisor and try again + emitter.instruction("b __rt_base_convert_scale"); // continue scaling the divisor upwards + + emitter.label("__rt_base_convert_reduce"); + emitter.instruction("fcmp d2, d5"); // does the scaled divisor still fit in the remainder? + emitter.instruction("b.lt __rt_base_convert_no_sub"); // skip the subtraction when it does not fit + emitter.instruction("fsub d2, d2, d5"); // subtract it; both operands are within a factor of two, so this is exact + emitter.label("__rt_base_convert_no_sub"); + emitter.instruction("fcmp d5, d1"); // has the divisor been halved back down to the base? + emitter.instruction("b.le __rt_base_convert_mod_done"); // the remainder is now below the base + emitter.instruction("fmov d7, #0.5"); // exact halving factor + emitter.instruction("fmul d5, d5, d7"); // halve the scaled divisor, exactly + emitter.instruction("b __rt_base_convert_reduce"); // reduce against the next lower scale + + emitter.label("__rt_base_convert_mod_done"); + emitter.instruction("fcvtzs x13, d2"); // php-src truncates the remainder to a digit index + emitter.instruction("cmp x13, #10"); // does this digit need a letter rather than a numeral? + emitter.instruction("b.lo __rt_base_convert_numeral"); // digits 0-9 use the ASCII numerals + emitter.instruction("add w13, w13, #87"); // map digits 10-35 to lowercase 'a'-'z' + emitter.instruction("b __rt_base_convert_store"); // the digit character is ready to store + emitter.label("__rt_base_convert_numeral"); + emitter.instruction("add w13, w13, #48"); // map digits 0-9 to ASCII '0'-'9' + emitter.label("__rt_base_convert_store"); + emitter.instruction("strb w13, [x11, #-1]!"); // store the digit least-significant-first, walking backwards + emitter.instruction("add x12, x12, #1"); // count the digit just emitted + emitter.instruction("fdiv d0, d0, d1"); // php-src divides WITHOUT re-flooring, which is what makes the render lossy + emitter.instruction(&format!("cmp x12, #{MAX_FLOAT_DIGITS}")); // php-src's digit buffer holds at most 64 characters + emitter.instruction("b.hs __rt_base_convert_emit"); // stop once the buffer is full + emitter.instruction("fabs d3, d0"); // php-src continues while fabs(value) >= 1 + emitter.instruction("fmov d4, #1.0"); // materialize the loop's lower bound + emitter.instruction("fcmp d3, d4"); // is there still a whole digit left to render? + emitter.instruction("b.ge __rt_base_convert_digit"); // render the next digit + + emitter.label("__rt_base_convert_emit"); + emitter.instruction("stp x11, x12, [sp, #64]"); // save the first-digit pointer and the digit count across the reservation + emitter.instruction("mov x0, x12"); // reserve exactly as many bytes as the rendered result needs + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the rendered digits + emitter.instruction("ldp x11, x12, [sp, #64]"); // reload the first-digit pointer and digit count after the reservation + emitter.instruction("mov x1, x0"); // the reservation start is the published result pointer + emitter.instruction("mov x2, x12"); // the digit count is the published result length + emitter.instruction("mov x13, #0"); // start copying at the first rendered digit + + emitter.label("__rt_base_convert_copy"); + emitter.instruction("cmp x13, x12"); // have all rendered digits been copied out? + emitter.instruction("b.hs __rt_base_convert_copied"); // finish once the whole result has been copied + emitter.instruction("ldrb w14, [x11, x13]"); // load the next rendered digit from the frame buffer + emitter.instruction("strb w14, [x0, x13]"); // store it into the reserved result storage + emitter.instruction("add x13, x13, #1"); // advance the copy index + emitter.instruction("b __rt_base_convert_copy"); // copy the next rendered digit + + emitter.label("__rt_base_convert_copied"); + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the base_convert helper frame + emitter.instruction("ret"); // return the rendered digits as a PHP string pair + + // -- infinite parse result: php-src warns and returns an empty string -- + emitter.label("__rt_base_convert_too_large"); + emitter.instruction("mov x0, #0"); // an empty result needs no storage + emitter.instruction("bl __rt_concat_reserve"); // still take a valid zero-length reservation + emitter.instruction("mov x1, x0"); // the reservation start is the published result pointer + emitter.instruction("mov x2, #0"); // the empty result has zero length + emitter.instruction("bl __rt_concat_publish"); // publish the empty result without moving the scratch offset + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the base_convert helper frame + emitter.instruction("ret"); // return the empty string as a PHP string pair +} + +/// Emits `__rt_base_convert` for x86_64 Linux using the System V ABI. +/// +/// The 64-byte digit buffer sits at `[rbp-128, rbp-64)` and the spill slots at +/// `[rbp-40]`/`[rbp-48]`, deliberately clear of the `[rbp-8]`..`[rbp-24]` window other +/// runtime emitters reserve for pushed callee-saved registers. `fabs(value) >= 1` is tested +/// on the integer bit pattern, which is order-preserving for finite magnitudes and avoids +/// needing a separate sign-mask XMM register. +fn emit_base_convert_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: base_convert ---"); + emitter.label_global("__rt_base_convert"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the nested calls + emitter.instruction("mov rbp, rsp"); // establish the base_convert helper frame pointer + emitter.instruction("sub rsp, 144"); // reserve the digit buffer and spill slots, keeping the stack 16-byte aligned + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the target base across the parse call + emitter.instruction("call __rt_base_to_number"); // parse the numeral exactly like php-src's _php_math_basetozval + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // reload the target base after the parse + emitter.instruction("test rax, rax"); // did the parse widen the value to a float? + emitter.instruction("jnz __rt_base_convert_float_linux_x86_64"); // a widened parse result renders through php-src's lossy float loop + + // -- integer result: render it exactly, like dechex/decbin/decoct do -- + emitter.instruction("mov rax, rdx"); // the parsed integer is the value to render + emitter.instruction("mov rdi, rcx"); // render it in the requested target base + emitter.instruction("call __rt_dec_to_base"); // reuse the shared unsigned integer-to-base renderer + emitter.instruction("add rsp, 144"); // release the base_convert helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the rendered digits as a PHP string pair + + // -- float result: reproduce php-src's floor + fmod digit loop bit for bit -- + emitter.label("__rt_base_convert_float_linux_x86_64"); + emitter.instruction("roundsd xmm0, xmm0, 1"); // php-src floors the widened value before rendering it + emitter.instruction("movq rax, xmm0"); // inspect the value's bit pattern to reject infinities + emitter.instruction(&format!("mov r8, 0x{F64_ABS_MASK:x}")); // materialize the IEEE-754 sign mask + emitter.instruction("and rax, r8"); // drop the sign bit to compare the magnitude + emitter.instruction(&format!("mov r8, 0x{F64_INFINITY_BITS:x}")); // materialize the infinity bit pattern + emitter.instruction("cmp rax, r8"); // did the parse overflow to infinity? + emitter.instruction("je __rt_base_convert_too_large_linux_x86_64"); // php-src returns an empty string for a value it cannot render + emitter.instruction("cvtsi2sd xmm1, rcx"); // keep the target base available as a double divisor + emitter.instruction(&format!("mov r8, 0x{F64_HALF_BITS:x}")); // materialize the exact halving factor + emitter.instruction("movq xmm7, r8"); // keep 0.5 available for the inline fmod reduction + emitter.instruction("lea r9, [rbp - 64]"); // point the digit cursor just past the end of the digit buffer + emitter.instruction("xor r10d, r10d"); // start with no digits emitted + + emitter.label("__rt_base_convert_digit_linux_x86_64"); + // -- inline exact fmod(xmm0, xmm1) into xmm2 by scaled subtraction -- + emitter.instruction("movapd xmm2, xmm0"); // the running remainder starts as the whole value + emitter.instruction("comisd xmm2, xmm1"); // is the value already smaller than the base? + emitter.instruction("jb __rt_base_convert_mod_done_linux_x86_64"); // then it is its own remainder + emitter.instruction("movapd xmm5, xmm1"); // scaled divisor starts at the base itself + + emitter.label("__rt_base_convert_scale_linux_x86_64"); + emitter.instruction("movapd xmm6, xmm5"); // copy the scaled divisor before doubling it + emitter.instruction("addsd xmm6, xmm5"); // double the scaled divisor, exactly + emitter.instruction("comisd xmm6, xmm2"); // has the scaled divisor passed the remainder? + emitter.instruction("ja __rt_base_convert_reduce_linux_x86_64"); // start reducing once doubling would overshoot + emitter.instruction("movapd xmm5, xmm6"); // keep the doubled divisor and try again + emitter.instruction("jmp __rt_base_convert_scale_linux_x86_64"); // continue scaling the divisor upwards + + emitter.label("__rt_base_convert_reduce_linux_x86_64"); + emitter.instruction("comisd xmm2, xmm5"); // does the scaled divisor still fit in the remainder? + emitter.instruction("jb __rt_base_convert_no_sub_linux_x86_64"); // skip the subtraction when it does not fit + emitter.instruction("subsd xmm2, xmm5"); // subtract it; both operands are within a factor of two, so this is exact + + emitter.label("__rt_base_convert_no_sub_linux_x86_64"); + emitter.instruction("comisd xmm5, xmm1"); // has the divisor been halved back down to the base? + emitter.instruction("jbe __rt_base_convert_mod_done_linux_x86_64"); // the remainder is now below the base + emitter.instruction("mulsd xmm5, xmm7"); // halve the scaled divisor, exactly + emitter.instruction("jmp __rt_base_convert_reduce_linux_x86_64"); // reduce against the next lower scale + + emitter.label("__rt_base_convert_mod_done_linux_x86_64"); + emitter.instruction("cvttsd2si rax, xmm2"); // php-src truncates the remainder to a digit index + emitter.instruction("cmp rax, 10"); // does this digit need a letter rather than a numeral? + emitter.instruction("jb __rt_base_convert_numeral_linux_x86_64"); // digits 0-9 use the ASCII numerals + emitter.instruction("add rax, 87"); // map digits 10-35 to lowercase 'a'-'z' + emitter.instruction("jmp __rt_base_convert_store_linux_x86_64"); // the digit character is ready to store + emitter.label("__rt_base_convert_numeral_linux_x86_64"); + emitter.instruction("add rax, 48"); // map digits 0-9 to ASCII '0'-'9' + emitter.label("__rt_base_convert_store_linux_x86_64"); + emitter.instruction("sub r9, 1"); // walk the digit cursor backwards by one character + emitter.instruction("mov BYTE PTR [r9], al"); // store the digit least-significant-first + emitter.instruction("add r10, 1"); // count the digit just emitted + emitter.instruction("divsd xmm0, xmm1"); // php-src divides WITHOUT re-flooring, which is what makes the render lossy + emitter.instruction(&format!("cmp r10, {MAX_FLOAT_DIGITS}")); // php-src's digit buffer holds at most 64 characters + emitter.instruction("jae __rt_base_convert_emit_linux_x86_64"); // stop once the buffer is full + emitter.instruction("movq rax, xmm0"); // php-src continues while fabs(value) >= 1 + emitter.instruction(&format!("mov r11, 0x{F64_ABS_MASK:x}")); // materialize the IEEE-754 sign mask + emitter.instruction("and rax, r11"); // compare magnitudes, which is order-preserving on the bit pattern + emitter.instruction(&format!("mov r11, 0x{F64_ONE_BITS:x}")); // materialize the bit pattern of 1.0 + emitter.instruction("cmp rax, r11"); // is there still a whole digit left to render? + emitter.instruction("jae __rt_base_convert_digit_linux_x86_64"); // render the next digit + + emitter.label("__rt_base_convert_emit_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 40], r9"); // save the first-digit pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save the digit count across the reservation call + emitter.instruction("mov rax, r10"); // reserve exactly as many bytes as the rendered result needs + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the rendered digits + emitter.instruction("mov r9, QWORD PTR [rbp - 40]"); // reload the first-digit pointer after the reservation + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the digit count after the reservation + emitter.instruction("xor ecx, ecx"); // start copying at the first rendered digit + + emitter.label("__rt_base_convert_copy_linux_x86_64"); + emitter.instruction("cmp rcx, r10"); // have all rendered digits been copied out? + emitter.instruction("jae __rt_base_convert_copied_linux_x86_64"); // finish once the whole result has been copied + emitter.instruction("mov r8b, BYTE PTR [r9 + rcx]"); // load the next rendered digit from the frame buffer + emitter.instruction("mov BYTE PTR [rax + rcx], r8b"); // store it into the reserved result storage + emitter.instruction("add rcx, 1"); // advance the copy index + emitter.instruction("jmp __rt_base_convert_copy_linux_x86_64"); // copy the next rendered digit + + emitter.label("__rt_base_convert_copied_linux_x86_64"); + emitter.instruction("mov rdx, r10"); // the digit count is the published result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 144"); // release the base_convert helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the rendered digits as a PHP string pair + + // -- infinite parse result: php-src warns and returns an empty string -- + emitter.label("__rt_base_convert_too_large_linux_x86_64"); + emitter.instruction("xor eax, eax"); // an empty result needs no storage + emitter.instruction("call __rt_concat_reserve"); // still take a valid zero-length reservation + emitter.instruction("xor edx, edx"); // the empty result has zero length + emitter.instruction("call __rt_concat_publish"); // publish the empty result without moving the scratch offset + emitter.instruction("add rsp, 144"); // release the base_convert helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the empty string as a PHP string pair +} diff --git a/src/codegen_support/runtime/strings/base_to_number.rs b/src/codegen_support/runtime/strings/base_to_number.rs new file mode 100644 index 0000000000..92429d6797 --- /dev/null +++ b/src/codegen_support/runtime/strings/base_to_number.rs @@ -0,0 +1,202 @@ +//! Purpose: +//! Emits the `__rt_base_to_number` runtime helper assembly shared by PHP's `hexdec`, +//! `bindec`, and `octdec` builtins: parses digits of one base into an `int`, widening to a +//! `float` exactly where reference PHP does. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - Mirrors php-src's `_php_math_basetozval`: characters that are not digits of the requested +//! base are IGNORED rather than terminating the scan (`hexdec("a0z") === 160`), and the +//! integer accumulator switches to `double` as soon as the next digit would push it past +//! `PHP_INT_MAX`. That threshold is `ZEND_LONG_MAX`, not `ZEND_ULONG_MAX`, which is why +//! `hexdec("ffffffffffffffff")` is a float in reference PHP. +//! - The `0x`/`0b`/`0o` prefixes php-src strips before scanning are deliberately NOT special +//! cased: for these three builtins the stripped characters are always either a leading `0` +//! or a letter that is not a digit of that base, so ignoring them yields the identical +//! value. php-src's `E_DEPRECATED` for ignored characters has no counterpart in elephc, +//! which emits no deprecation diagnostics at all. +//! - The helper allocates nothing and calls nothing, so it needs no frame. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_base_to_number` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = string pointer, `x2` = string length, `x3` = base (2..36). +/// Output: `x0` = 0 when the result is an integer and 1 when it widened to a float, +/// `x1` = the integer result, `d0` = the float result. +/// +/// ABI (x86_64 System V): +/// Input: `rdi` = string pointer, `rsi` = string length, `rdx` = base (2..36). +/// Output: `rax` = 0 when the result is an integer and 1 when it widened to a float, +/// `rdx` = the integer result, `xmm0` = the float result. +/// +/// An empty string, or one made entirely of characters that are not digits of the base, +/// yields the integer `0` — the same as reference PHP. +pub fn emit_base_to_number(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_base_to_number_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: base_to_number ---"); + emitter.label_global("__rt_base_to_number"); + + // -- derive the php-src overflow thresholds from PHP_INT_MAX and the requested base -- + abi::emit_load_int_immediate(emitter, "x11", i64::MAX); + emitter.instruction("udiv x4, x11, x3"); // x4 = PHP_INT_MAX / base, the last accumulator that can still take a digit + emitter.instruction("msub x5, x4, x3, x11"); // x5 = PHP_INT_MAX % base, the largest digit that threshold still accepts + emitter.instruction("mov x6, #0"); // start the integer accumulator at zero + emitter.instruction("mov x7, #0"); // start in integer mode; 1 means the result widened to a float + emitter.instruction("fmov d0, xzr"); // start the float accumulator at 0.0 + emitter.instruction("ucvtf d2, x3"); // keep the base available as a double for the float accumulation + + emitter.label("__rt_base_to_number_loop"); + emitter.instruction("cbz x2, __rt_base_to_number_done"); // stop once every input byte has been inspected + emitter.instruction("ldrb w9, [x1], #1"); // load the next input byte and advance the cursor + emitter.instruction("sub x2, x2, #1"); // record that one input byte has been consumed + + // -- decode one digit, ignoring every character that is not one -- + emitter.instruction("sub w10, w9, #48"); // try the ASCII numerals first + emitter.instruction("cmp w10, #9"); // is the byte in '0'..'9' (unsigned, so lower bytes wrap high)? + emitter.instruction("b.ls __rt_base_to_number_digit"); // an ASCII numeral decodes directly + emitter.instruction("cmp w9, #65"); // is the byte below 'A'? + emitter.instruction("b.lo __rt_base_to_number_loop"); // punctuation between the numerals and 'A' is ignored + emitter.instruction("cmp w9, #90"); // is the byte at or below 'Z'? + emitter.instruction("b.hi __rt_base_to_number_lower"); // try the lowercase letters instead + emitter.instruction("sub w10, w9, #55"); // map 'A'..'Z' to digit values 10..35 + emitter.instruction("b __rt_base_to_number_digit"); // the uppercase letter decoded to a digit + emitter.label("__rt_base_to_number_lower"); + emitter.instruction("cmp w9, #97"); // is the byte below 'a'? + emitter.instruction("b.lo __rt_base_to_number_loop"); // punctuation between 'Z' and 'a' is ignored + emitter.instruction("cmp w9, #122"); // is the byte above 'z'? + emitter.instruction("b.hi __rt_base_to_number_loop"); // bytes past 'z' are ignored + emitter.instruction("sub w10, w9, #87"); // map 'a'..'z' to digit values 10..35 + + emitter.label("__rt_base_to_number_digit"); + emitter.instruction("cmp x10, x3"); // is the decoded digit valid in the requested base? + emitter.instruction("b.hs __rt_base_to_number_loop"); // digits outside the base are ignored, exactly like php-src + emitter.instruction("cbnz x7, __rt_base_to_number_float"); // once widened, every further digit accumulates as a float + emitter.instruction("cmp x6, x4"); // would this digit push the accumulator past PHP_INT_MAX? + emitter.instruction("b.lo __rt_base_to_number_int"); // below the threshold the integer accumulator always fits + emitter.instruction("b.hi __rt_base_to_number_widen"); // above the threshold the accumulator must widen + emitter.instruction("cmp x10, x5"); // at the threshold only digits up to the remainder still fit + emitter.instruction("b.hi __rt_base_to_number_widen"); // a larger digit forces the widening to float + + emitter.label("__rt_base_to_number_int"); + emitter.instruction("madd x6, x6, x3, x10"); // accumulate the digit into the integer result + emitter.instruction("b __rt_base_to_number_loop"); // continue scanning the remaining input bytes + + emitter.label("__rt_base_to_number_widen"); + emitter.instruction("ucvtf d0, x6"); // seed the float accumulator from the integer digits parsed so far + emitter.instruction("mov x7, #1"); // remember that the result is now a float + + emitter.label("__rt_base_to_number_float"); + emitter.instruction("fmul d0, d0, d2"); // shift the float accumulator up by one digit position + emitter.instruction("ucvtf d3, x10"); // convert the current digit for the float accumulation + emitter.instruction("fadd d0, d0, d3"); // accumulate the digit into the float result + emitter.instruction("b __rt_base_to_number_loop"); // continue scanning the remaining input bytes + + emitter.label("__rt_base_to_number_done"); + emitter.instruction("cbnz x7, __rt_base_to_number_float_result"); // report a widened result through the float register + emitter.instruction("mov x0, #0"); // report that the result is a PHP integer + emitter.instruction("mov x1, x6"); // return the accumulated integer value + emitter.instruction("ret"); // hand the integer result back to the caller + + emitter.label("__rt_base_to_number_float_result"); + emitter.instruction("mov x0, #1"); // report that the result widened to a PHP float + emitter.instruction("ret"); // the float result is already in d0 +} + +/// Emits `__rt_base_to_number` for x86_64 Linux using the System V ABI. +/// +/// The base is moved into `rcx` up front so `rdx` is free for the unsigned division that +/// derives the overflow thresholds and, afterwards, for the decoded digit. +fn emit_base_to_number_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: base_to_number ---"); + emitter.label_global("__rt_base_to_number"); + + // -- derive the php-src overflow thresholds from PHP_INT_MAX and the requested base -- + emitter.instruction("mov rcx, rdx"); // keep the requested base in a stable register + abi::emit_load_int_immediate(emitter, "rax", i64::MAX); + emitter.instruction("xor edx, edx"); // clear the high dividend half before the unsigned division + emitter.instruction("div rcx"); // rax = PHP_INT_MAX / base, rdx = PHP_INT_MAX % base + emitter.instruction("mov r8, rax"); // r8 = the last accumulator that can still take a digit + emitter.instruction("mov r9, rdx"); // r9 = the largest digit that threshold still accepts + emitter.instruction("xor r10d, r10d"); // start the integer accumulator at zero + emitter.instruction("xor r11d, r11d"); // start in integer mode; 1 means the result widened to a float + emitter.instruction("pxor xmm0, xmm0"); // start the float accumulator at 0.0 + emitter.instruction("cvtsi2sd xmm1, rcx"); // keep the base available as a double for the float accumulation + + emitter.label("__rt_base_to_number_loop_linux_x86_64"); + emitter.instruction("test rsi, rsi"); // stop once every input byte has been inspected + emitter.instruction("jz __rt_base_to_number_done_linux_x86_64"); // finish when the input string is exhausted + emitter.instruction("movzx rax, BYTE PTR [rdi]"); // load the next input byte + emitter.instruction("add rdi, 1"); // advance the input cursor + emitter.instruction("sub rsi, 1"); // record that one input byte has been consumed + + // -- decode one digit, ignoring every character that is not one -- + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its numeral value + emitter.instruction("sub rdx, 48"); // try the ASCII numerals first + emitter.instruction("cmp rdx, 9"); // is the byte in '0'..'9' (unsigned, so lower bytes wrap high)? + emitter.instruction("jbe __rt_base_to_number_digit_linux_x86_64"); // an ASCII numeral decodes directly + emitter.instruction("cmp rax, 65"); // is the byte below 'A'? + emitter.instruction("jb __rt_base_to_number_loop_linux_x86_64"); // punctuation between the numerals and 'A' is ignored + emitter.instruction("cmp rax, 90"); // is the byte above 'Z'? + emitter.instruction("ja __rt_base_to_number_lower_linux_x86_64"); // try the lowercase letters instead + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its letter value + emitter.instruction("sub rdx, 55"); // map 'A'..'Z' to digit values 10..35 + emitter.instruction("jmp __rt_base_to_number_digit_linux_x86_64"); // the uppercase letter decoded to a digit + emitter.label("__rt_base_to_number_lower_linux_x86_64"); + emitter.instruction("cmp rax, 97"); // is the byte below 'a'? + emitter.instruction("jb __rt_base_to_number_loop_linux_x86_64"); // punctuation between 'Z' and 'a' is ignored + emitter.instruction("cmp rax, 122"); // is the byte above 'z'? + emitter.instruction("ja __rt_base_to_number_loop_linux_x86_64"); // bytes past 'z' are ignored + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its letter value + emitter.instruction("sub rdx, 87"); // map 'a'..'z' to digit values 10..35 + + emitter.label("__rt_base_to_number_digit_linux_x86_64"); + emitter.instruction("cmp rdx, rcx"); // is the decoded digit valid in the requested base? + emitter.instruction("jae __rt_base_to_number_loop_linux_x86_64"); // digits outside the base are ignored, exactly like php-src + emitter.instruction("test r11, r11"); // has the accumulator already widened to a float? + emitter.instruction("jnz __rt_base_to_number_float_linux_x86_64"); // once widened, every further digit accumulates as a float + emitter.instruction("cmp r10, r8"); // would this digit push the accumulator past PHP_INT_MAX? + emitter.instruction("jb __rt_base_to_number_int_linux_x86_64"); // below the threshold the integer accumulator always fits + emitter.instruction("ja __rt_base_to_number_widen_linux_x86_64"); // above the threshold the accumulator must widen + emitter.instruction("cmp rdx, r9"); // at the threshold only digits up to the remainder still fit + emitter.instruction("ja __rt_base_to_number_widen_linux_x86_64"); // a larger digit forces the widening to float + + emitter.label("__rt_base_to_number_int_linux_x86_64"); + emitter.instruction("mov rax, r10"); // stage the accumulator for the digit shift + emitter.instruction("imul rax, rcx"); // shift the accumulator up by one digit position + emitter.instruction("add rax, rdx"); // accumulate the digit into the integer result + emitter.instruction("mov r10, rax"); // keep the updated integer accumulator + emitter.instruction("jmp __rt_base_to_number_loop_linux_x86_64"); // continue scanning the remaining input bytes + + emitter.label("__rt_base_to_number_widen_linux_x86_64"); + emitter.instruction("cvtsi2sd xmm0, r10"); // seed the float accumulator from the integer digits parsed so far + emitter.instruction("mov r11, 1"); // remember that the result is now a float + + emitter.label("__rt_base_to_number_float_linux_x86_64"); + emitter.instruction("mulsd xmm0, xmm1"); // shift the float accumulator up by one digit position + emitter.instruction("cvtsi2sd xmm2, rdx"); // convert the current digit for the float accumulation + emitter.instruction("addsd xmm0, xmm2"); // accumulate the digit into the float result + emitter.instruction("jmp __rt_base_to_number_loop_linux_x86_64"); // continue scanning the remaining input bytes + + emitter.label("__rt_base_to_number_done_linux_x86_64"); + emitter.instruction("test r11, r11"); // did the accumulator widen to a float? + emitter.instruction("jnz __rt_base_to_number_float_result_linux_x86_64"); // report a widened result through the float register + emitter.instruction("xor eax, eax"); // report that the result is a PHP integer + emitter.instruction("mov rdx, r10"); // return the accumulated integer value + emitter.instruction("ret"); // hand the integer result back to the caller + + emitter.label("__rt_base_to_number_float_result_linux_x86_64"); + emitter.instruction("mov rax, 1"); // report that the result widened to a PHP float + emitter.instruction("ret"); // the float result is already in xmm0 +} diff --git a/src/codegen_support/runtime/strings/bin2hex.rs b/src/codegen_support/runtime/strings/bin2hex.rs index 9c6c3b2962..cee2b1fde1 100644 --- a/src/codegen_support/runtime/strings/bin2hex.rs +++ b/src/codegen_support/runtime/strings/bin2hex.rs @@ -7,15 +7,18 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - The `2 * len` result is reserved through `__rt_concat_reserve` before the first store, so +//! inputs whose hexadecimal expansion exceeds the 64 KiB concat scratch fall back to heap storage. use crate::codegen_support::{emit::Emitter, platform::Arch}; -use crate::codegen_support::abi; /// Emits the `__rt_bin2hex` runtime helper for the `bin2hex` builtin. /// Dispatches to target-specific implementations. On ARM64, uses x1/x2 for input /// string pointer/length and returns result pointer/length in x1/x2. On x86_64 Linux, /// uses rax/rdx for string result, rsi for source pointer, rdx for source length. -/// Both variants append to the shared `_concat_buf` global and advance `_concat_off`. +/// Both variants reserve the exact `2 * len` result through `__rt_concat_reserve` +/// (concat scratch while it fits, owned heap storage otherwise) and finish through +/// `__rt_concat_publish`, which advances `_concat_off` only for scratch-backed results. pub fn emit_bin2hex(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_bin2hex_linux_x86_64(emitter); @@ -26,11 +29,17 @@ pub fn emit_bin2hex(emitter: &mut Emitter) { emitter.comment("--- runtime: bin2hex ---"); emitter.label_global("__rt_bin2hex"); - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the exact 2-bytes-per-input-byte result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the bin2hex helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("adds x0, x2, x2"); // compute the hexadecimal result size as 2 * source length + emitter.instruction("b.cs __rt_bin2hex_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the hexadecimal result + emitter.instruction("mov x9, x0"); // destination cursor + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining count emitter.label("__rt_bin2hex_loop"); @@ -62,10 +71,14 @@ pub fn emit_bin2hex(emitter: &mut Emitter) { emitter.label("__rt_bin2hex_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the bin2hex helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_bin2hex_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits `__rt_bin2hex` for x86_64 Linux using the System V ABI. @@ -74,12 +87,19 @@ fn emit_bin2hex_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: bin2hex ---"); emitter.label_global("__rt_bin2hex"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer offset before appending the hexadecimal bytes - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // load the base address of the shared concat buffer - emitter.instruction("add r9, r8"); // compute the destination pointer at the current concat-buffer tail + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the source byte count across the reservation call + emitter.instruction("mov rax, rdx"); // seed the result size from the source byte count + emitter.instruction("add rax, rax"); // compute the hexadecimal result size as 2 * source length + emitter.instruction("jc __rt_bin2hex_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the hexadecimal result + emitter.instruction("mov r9, rax"); // compute the destination pointer at the reserved result start emitter.instruction("mov r10, r9"); // preserve the hexadecimal string start pointer for the return value - emitter.instruction("mov rcx, rdx"); // copy the source byte count into a decrementing loop counter - emitter.instruction("mov rsi, rax"); // copy the source pointer into a cursor register for byte-by-byte reads + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // copy the source byte count into a decrementing loop counter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // copy the source pointer into a cursor register for byte-by-byte reads emitter.label("__rt_bin2hex_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been converted to two hex characters @@ -115,11 +135,14 @@ fn emit_bin2hex_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_bin2hex_done_linux_x86_64"); emitter.instruction("mov rax, r10"); // return the hexadecimal string start pointer in the standard x86_64 string result register - emitter.instruction("mov rdx, r9"); // copy the concat-buffer tail into the length scratch register + emitter.instruction("mov rdx, r9"); // copy the destination cursor into the length scratch register emitter.instruction("sub rdx, r10"); // compute the hexadecimal string length from the written byte count - emitter.instruction("mov r8, r9"); // copy the absolute concat-buffer tail before normalizing it back to a shared offset - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // load the concat-buffer base so the shared offset can stay relative - emitter.instruction("sub r8, r11"); // convert the absolute concat-buffer tail back into the shared relative offset - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated relative concat-buffer offset for later string appenders + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the bin2hex spill slots before returning the hexadecimal string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the hexadecimal string emitter.instruction("ret"); // return the hexadecimal string through the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_bin2hex_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/chunk_split.rs b/src/codegen_support/runtime/strings/chunk_split.rs new file mode 100644 index 0000000000..aadaea7625 --- /dev/null +++ b/src/codegen_support/runtime/strings/chunk_split.rs @@ -0,0 +1,211 @@ +//! Purpose: +//! Emits the `__rt_chunk_split` runtime helper assembly for PHP's `chunk_split`: copies the +//! subject in fixed-size pieces, appending the separator after every piece including the last. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - php-src appends the separator after the trailing partial chunk too, and its +//! `chunklen > srclen` back-compat branch means an EMPTY subject still yields exactly one +//! separator (`chunk_split("", 3, "-") === "-"`). The copy loop is therefore a do-while: +//! one iteration always runs, copying `min(chunklen, remaining)` bytes plus the separator. +//! - The exact result size is `srclen + parts * seplen` where `parts` is the number of loop +//! iterations (`ceil(srclen / chunklen)`, floored at 1). It is reserved through +//! `__rt_concat_reserve` before the first store, so results past the 64 KiB concat scratch +//! fall back to owned heap storage instead of running off the buffer. +//! - `$length < 1` never reaches this helper: the EIR lowering raises php-src's `ValueError` +//! before the call, which is also what keeps the division below well defined. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_chunk_split` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = subject pointer, `x2` = subject length, `x3` = chunk length (>= 1), +/// `x4` = separator pointer, `x5` = separator length. +/// Output: `x1` = result pointer, `x2` = result length. +/// +/// ABI (x86_64 System V): +/// Input: `rax` = subject pointer, `rdx` = subject length, `rdi` = chunk length (>= 1), +/// `rcx` = separator pointer, `r8` = separator length. +/// Output: `rax` = result pointer, `rdx` = result length. +/// +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// A size computation that would wrap reports PHP's allocation-overflow fatal instead. +pub fn emit_chunk_split(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_chunk_split_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: chunk_split ---"); + emitter.label_global("__rt_chunk_split"); + + // -- preserve the borrowed subject and separator across the reservation call -- + emitter.instruction("sub sp, sp, #80"); // allocate spill space for the subject, separator, and result start + emitter.instruction("stp x29, x30, [sp, #64]"); // save the frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #64"); // establish the chunk_split helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the subject pointer and length + emitter.instruction("stp x3, x4, [sp, #16]"); // save the chunk length and separator pointer + emitter.instruction("str x5, [sp, #32]"); // save the separator length + + // -- count the separators the copy loop will emit -- + emitter.instruction("udiv x9, x2, x3"); // whole chunks = subject length / chunk length + emitter.instruction("msub x10, x9, x3, x2"); // trailing remainder = subject length - chunks * chunk length + emitter.instruction("cmp x10, #0"); // is there a trailing partial chunk? + emitter.instruction("cinc x9, x9, ne"); // a partial chunk contributes one more separator + emitter.instruction("cmp x9, #0"); // did an empty subject leave no chunks at all? + emitter.instruction("csinc x9, x9, xzr, ne"); // php-src's back-compat branch still emits one separator for an empty subject + + // -- reserve the exact subject + separators result before writing anything -- + emitter.instruction("umulh x11, x9, x5"); // capture the high half of the separators * separator length product + emitter.instruction("cbnz x11, __rt_chunk_split_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x12, x9, x5"); // total separator bytes in the finished result + emitter.instruction("adds x0, x2, x12"); // result size = subject length + total separator bytes + emitter.instruction("b.cs __rt_chunk_split_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the split result + emitter.instruction("mov x13, x0"); // destination cursor + emitter.instruction("str x0, [sp, #40]"); // save the result start for the published pointer + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed subject pointer and remaining length + emitter.instruction("ldp x3, x4, [sp, #16]"); // reload the chunk length and separator pointer + emitter.instruction("ldr x5, [sp, #32]"); // reload the separator length + + // -- do-while: one iteration always runs so an empty subject still emits a separator -- + emitter.label("__rt_chunk_split_chunk"); + emitter.instruction("cmp x2, x3"); // does a whole chunk still fit in the remaining subject? + emitter.instruction("csel x9, x3, x2, hs"); // copy a whole chunk, or the shorter trailing remainder + emitter.instruction("mov x10, #0"); // start copying at the first byte of this chunk + + emitter.label("__rt_chunk_split_copy"); + emitter.instruction("cmp x10, x9"); // has the whole chunk been copied? + emitter.instruction("b.hs __rt_chunk_split_copied"); // move on to the separator once the chunk is copied + emitter.instruction("ldrb w11, [x1, x10]"); // load the next subject byte + emitter.instruction("strb w11, [x13, x10]"); // store it at the same offset inside the result + emitter.instruction("add x10, x10, #1"); // advance the copy index + emitter.instruction("b __rt_chunk_split_copy"); // copy the next subject byte + + emitter.label("__rt_chunk_split_copied"); + emitter.instruction("add x1, x1, x9"); // advance the subject cursor past the copied chunk + emitter.instruction("add x13, x13, x9"); // advance the destination cursor past the copied chunk + emitter.instruction("sub x2, x2, x9"); // record how much subject is still unconsumed + emitter.instruction("mov x10, #0"); // start copying at the first separator byte + + emitter.label("__rt_chunk_split_sep"); + emitter.instruction("cmp x10, x5"); // has the whole separator been copied? + emitter.instruction("b.hs __rt_chunk_split_sep_done"); // the chunk plus its separator are complete + emitter.instruction("ldrb w11, [x4, x10]"); // load the next separator byte + emitter.instruction("strb w11, [x13, x10]"); // store it at the same offset inside the result + emitter.instruction("add x10, x10, #1"); // advance the copy index + emitter.instruction("b __rt_chunk_split_sep"); // copy the next separator byte + + emitter.label("__rt_chunk_split_sep_done"); + emitter.instruction("add x13, x13, x5"); // advance the destination cursor past the separator + emitter.instruction("cbnz x2, __rt_chunk_split_chunk"); // keep splitting while subject bytes remain + + emitter.instruction("ldr x1, [sp, #40]"); // return the split string start pointer + emitter.instruction("sub x2, x13, x1"); // the written byte count is the result length + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the chunk_split helper frame + emitter.instruction("ret"); // return the split string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_chunk_split_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe +} + +/// Emits `__rt_chunk_split` for x86_64 Linux using the System V ABI. +/// +/// The spill slots start at `[rbp-32]` so they stay clear of the `[rbp-8]`..`[rbp-24]` window +/// other runtime emitters reserve for pushed callee-saved registers. +fn emit_chunk_split_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: chunk_split ---"); + emitter.label_global("__rt_chunk_split"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed strings + emitter.instruction("sub rsp, 80"); // reserve aligned spill slots for the subject, separator, and result start + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the subject pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the subject length across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the chunk length across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the separator pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // save the separator length across the reservation call + + // -- count the separators the copy loop will emit -- + emitter.instruction("mov rax, rdx"); // the subject length is the dividend low half + emitter.instruction("xor edx, edx"); // clear the dividend high half before the unsigned division + emitter.instruction("div rdi"); // whole chunks in rax, trailing remainder in rdx + emitter.instruction("test rdx, rdx"); // is there a trailing partial chunk? + emitter.instruction("jz __rt_chunk_split_no_rest_linux_x86_64"); // skip the extra separator when the subject divides evenly + emitter.instruction("add rax, 1"); // a partial chunk contributes one more separator + + emitter.label("__rt_chunk_split_no_rest_linux_x86_64"); + emitter.instruction("test rax, rax"); // did an empty subject leave no chunks at all? + emitter.instruction("jnz __rt_chunk_split_have_parts_linux_x86_64"); // a non-empty subject already has its separator count + emitter.instruction("mov rax, 1"); // php-src's back-compat branch still emits one separator for an empty subject + + emitter.label("__rt_chunk_split_have_parts_linux_x86_64"); + // -- reserve the exact subject + separators result before writing anything -- + emitter.instruction("mul r8"); // total separator bytes = separators * separator length + emitter.instruction("jc __rt_chunk_split_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("add rax, QWORD PTR [rbp - 40]"); // result size = subject length + total separator bytes + emitter.instruction("jc __rt_chunk_split_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the split result + emitter.instruction("mov r9, rax"); // destination cursor + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the result start for the published pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload the borrowed subject pointer as a read cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // reload the subject length as the remaining counter + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the chunk length + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the separator pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the separator length + + // -- do-while: one iteration always runs so an empty subject still emits a separator -- + emitter.label("__rt_chunk_split_chunk_linux_x86_64"); + emitter.instruction("mov rax, rcx"); // assume the shorter trailing remainder is the piece to copy + emitter.instruction("cmp rcx, rdi"); // does a whole chunk still fit in the remaining subject? + emitter.instruction("cmovae rax, rdi"); // copy a whole chunk when one still fits + emitter.instruction("xor edx, edx"); // start copying at the first byte of this chunk + + emitter.label("__rt_chunk_split_copy_linux_x86_64"); + emitter.instruction("cmp rdx, rax"); // has the whole chunk been copied? + emitter.instruction("jae __rt_chunk_split_copied_linux_x86_64"); // move on to the separator once the chunk is copied + emitter.instruction("mov r8b, BYTE PTR [rsi + rdx]"); // load the next subject byte + emitter.instruction("mov BYTE PTR [r9 + rdx], r8b"); // store it at the same offset inside the result + emitter.instruction("add rdx, 1"); // advance the copy index + emitter.instruction("jmp __rt_chunk_split_copy_linux_x86_64"); // copy the next subject byte + + emitter.label("__rt_chunk_split_copied_linux_x86_64"); + emitter.instruction("add rsi, rax"); // advance the subject cursor past the copied chunk + emitter.instruction("add r9, rax"); // advance the destination cursor past the copied chunk + emitter.instruction("sub rcx, rax"); // record how much subject is still unconsumed + emitter.instruction("xor edx, edx"); // start copying at the first separator byte + + emitter.label("__rt_chunk_split_sep_linux_x86_64"); + emitter.instruction("cmp rdx, r11"); // has the whole separator been copied? + emitter.instruction("jae __rt_chunk_split_sep_done_linux_x86_64"); // the chunk plus its separator are complete + emitter.instruction("mov r8b, BYTE PTR [r10 + rdx]"); // load the next separator byte + emitter.instruction("mov BYTE PTR [r9 + rdx], r8b"); // store it at the same offset inside the result + emitter.instruction("add rdx, 1"); // advance the copy index + emitter.instruction("jmp __rt_chunk_split_sep_linux_x86_64"); // copy the next separator byte + + emitter.label("__rt_chunk_split_sep_done_linux_x86_64"); + emitter.instruction("add r9, r11"); // advance the destination cursor past the separator + emitter.instruction("test rcx, rcx"); // are there subject bytes left to split? + emitter.instruction("jnz __rt_chunk_split_chunk_linux_x86_64"); // keep splitting while subject bytes remain + + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // return the split string start pointer + emitter.instruction("mov rdx, r9"); // copy the destination cursor into the length scratch register + emitter.instruction("sub rdx, rax"); // the written byte count is the result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 80"); // release the chunk_split spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the split string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_chunk_split_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller +} diff --git a/src/codegen_support/runtime/strings/concat.rs b/src/codegen_support/runtime/strings/concat.rs index 7d6c448a62..0fb6178fab 100644 --- a/src/codegen_support/runtime/strings/concat.rs +++ b/src/codegen_support/runtime/strings/concat.rs @@ -7,16 +7,33 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. - +//! - Destination storage comes from `__rt_concat_reserve`, so a result that no longer fits the +//! fixed 64 KiB `_concat_buf` scratch buffer lands in an owned heap block instead of running +//! past the buffer end into the adjacent BSS globals. +//! - A heap-backed result is stamped with `CONCAT_TEMP_HEAP_KIND` so `__rt_str_persist` can +//! take it over in place rather than allocating a second copy of it. +//! - `left_len + right_len` is checked for unsigned wrap before the reservation, so a wrapped +//! total can never size a destination smaller than the bytes the copy loops write. + +use crate::codegen_support::runtime::strings::concat_scratch::{ + CONCAT_BUF_CAPACITY, CONCAT_TEMP_HEAP_KIND, +}; use crate::codegen_support::{emit::Emitter, platform::Arch}; /// Emits the `__rt_concat` runtime helper for concatenating two byte-strings. -/// Writes the concatenated result into the global `_concat_buf` at the current `_concat_off` -/// offset, then advances `_concat_off` by the total bytes written. +/// +/// Sizes the result through `__rt_concat_reserve` (concat scratch while it fits the shared +/// 64 KiB buffer, an owned heap block otherwise), copies both operands into the reservation, +/// and publishes the written length through `__rt_concat_publish`, which advances `_concat_off` +/// only for scratch-backed results. /// Dispatches to `emit_concat_linux_x86_64` on x86_64; uses the ARM64 path otherwise. /// /// Input: x1=left_ptr, x2=left_len, x3=right_ptr, x4=right_len -/// Output: x1=result_ptr (start of written region in _concat_buf), x2=result_len +/// Output: x1=result_ptr, x2=result_len +/// +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// A wrapped `left_len + right_len` reports PHP's allocation-overflow fatal through +/// `__rt_alloc_overflow` instead of under-sizing the destination. pub fn emit_concat(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_concat_linux_x86_64(emitter); @@ -35,19 +52,28 @@ pub fn emit_concat(emitter: &mut Emitter) { // -- save input arguments to stack -- emitter.instruction("stp x1, x2, [sp, #0]"); // save left string ptr and length emitter.instruction("stp x3, x4, [sp, #16]"); // save right string ptr and length - emitter.instruction("add x5, x2, x4"); // compute total result length + emitter.instruction("adds x5, x2, x4"); // compute total result length and record unsigned wrap + emitter.instruction("b.cs __rt_concat_size_overflow"); // a wrapped total can never describe the bytes the copy loops write emitter.instruction("str x5, [sp, #32]"); // save total length on stack - // -- get concat_buf write position -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // compute destination pointer: buf + offset - emitter.instruction("str x9, [sp, #40]"); // save result start pointer on stack + // -- reserve bounded destination storage instead of appending blindly at _concat_off -- + emitter.instruction("mov x0, x5"); // request storage for the full concatenated payload + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the result + emitter.instruction("str x0, [sp, #40]"); // save result start pointer on stack + + // -- stamp heap-backed results so __rt_str_persist can take them over in place -- + crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_buf"); + emitter.instruction("sub x7, x0, x6"); // compute the candidate scratch offset of the reservation + emitter.instruction(&format!("mov x8, #{}", CONCAT_BUF_CAPACITY)); // load the concat scratch capacity in bytes + emitter.instruction("cmp x7, x8"); // is the reservation outside the shared scratch window (unsigned)? + emitter.instruction("b.lo __rt_concat_dest_ready"); // scratch-backed reservations carry no heap header to stamp + emitter.instruction(&format!("mov x8, #{}", CONCAT_TEMP_HEAP_KIND)); // heap kind 7 = transient `.` operator temporary + emitter.instruction("str x8, [x0, #-8]"); // stamp the heap reservation as a concat temporary + emitter.label("__rt_concat_dest_ready"); // -- copy left string bytes -- emitter.instruction("ldp x1, x2, [sp, #0]"); // reload left ptr and length - emitter.instruction("mov x10, x9"); // set dest cursor to start of output + emitter.instruction("mov x10, x0"); // set dest cursor to start of output emitter.label("__rt_concat_cl"); emitter.instruction("cbz x2, __rt_concat_cr_setup"); // if no bytes left, move to right string emitter.instruction("ldrb w11, [x1], #1"); // load byte from left string, advance src @@ -65,26 +91,26 @@ pub fn emit_concat(emitter: &mut Emitter) { emitter.instruction("sub x4, x4, #1"); // decrement remaining right bytes emitter.instruction("b __rt_concat_cr"); // continue copying right string - // -- update concat_buf offset and return result -- + // -- publish the written length and return the result -- emitter.label("__rt_concat_done"); - emitter.instruction("ldr x5, [sp, #32]"); // reload total result length - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - emitter.instruction("add x8, x8, x5"); // advance offset by total length written - emitter.instruction("str x8, [x6]"); // store updated offset - - // -- set return values and restore frame -- emitter.instruction("ldr x1, [sp, #40]"); // return result pointer (start of output) emitter.instruction("ldr x2, [sp, #32]"); // return result length + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // deallocate stack frame emitter.instruction("ret"); // return to caller + + // -- fatal error: left_len + right_len does not fit a machine word -- + emitter.label("__rt_concat_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of `__rt_concat`. /// Uses the System V AMD64 ABI: left string in rax/rdx, right string in rdi/rsi. /// Result returned in rax (pointer) and rdx (length). -/// The global `_concat_buf` / `_concat_off` machinery is shared with the ARM64 path. +/// Behavior mirrors the ARM64 path: `__rt_concat_reserve` picks concat scratch or an owned +/// heap block, a heap-backed result is stamped as a transient `.` temporary, and +/// `__rt_concat_publish` advances `_concat_off` only for scratch-backed results. fn emit_concat_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: concat ---"); @@ -100,14 +126,25 @@ fn emit_concat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save right string length emitter.instruction("mov r8, rdx"); // seed total length from the left string length emitter.instruction("add r8, rsi"); // total length = left length + right length - emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save total length for offset update and return - - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load current concat write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r10, [r10 + r9]"); // compute destination pointer: concat_buf + offset - emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // save result start pointer for return - + emitter.instruction("jc __rt_concat_size_overflow_x86"); // a wrapped total can never describe the bytes the copy loops write + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save total length for the publish step and return + + // -- reserve bounded destination storage instead of appending blindly at _concat_off -- + emitter.instruction("mov rax, r8"); // request storage for the full concatenated payload + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the result + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save result start pointer for return + + // -- stamp heap-backed results so __rt_str_persist can take them over in place -- + crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_buf"); + emitter.instruction("mov r9, rax"); // copy the reservation before deriving its candidate scratch offset + emitter.instruction("sub r9, r8"); // compute the candidate scratch offset of the reservation + emitter.instruction(&format!("cmp r9, {}", CONCAT_BUF_CAPACITY)); // is the reservation outside the shared scratch window (unsigned)? + emitter.instruction("jb __rt_concat_dest_ready_x86"); // scratch-backed reservations carry no heap header to stamp + emitter.instruction(&format!("mov r8, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(CONCAT_TEMP_HEAP_KIND))); // materialize the transient-concat heap kind word with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r8"); // stamp the heap reservation as a concat temporary + emitter.label("__rt_concat_dest_ready_x86"); + + emitter.instruction("mov r10, rax"); // set the concat destination cursor to the start of the reservation emitter.instruction("mov r8, QWORD PTR [rbp - 8]"); // load left source pointer emitter.instruction("mov r9, QWORD PTR [rbp - 16]"); // load remaining left byte count emitter.label("__rt_concat_cl"); @@ -134,15 +171,16 @@ fn emit_concat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_concat_cr"); // continue copying right bytes emitter.label("__rt_concat_done"); - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // reload current concat write offset - emitter.instruction("add r9, QWORD PTR [rbp - 40]"); // advance offset by total bytes written - emitter.instruction("mov QWORD PTR [r8], r9"); // store updated concat write offset emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // return result pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // return result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 48"); // release concat local slots emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return concatenated string in rax/rdx + + // -- fatal error: left_len + right_len does not fit a machine word -- + emitter.label("__rt_concat_size_overflow_x86"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } #[cfg(test)] @@ -152,8 +190,9 @@ mod tests { use super::*; #[test] - /// Verifies that the x86_64 Linux concat path uses native byte-copy loops and returns - /// the result pointer/length in rax/rdx per the AMD64 ABI convention. + /// Verifies that the x86_64 Linux concat path reserves bounded destination storage, + /// uses native byte-copy loops, and returns the result pointer/length in rax/rdx per + /// the AMD64 ABI convention. fn test_emit_concat_linux_x86_64_uses_native_copy_loop() { let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); emit_concat(&mut emitter); @@ -161,7 +200,23 @@ mod tests { assert!(asm.contains("__rt_concat:\n")); assert!(asm.contains("mov QWORD PTR [rbp - 8], rax\n")); + assert!(asm.contains("call __rt_concat_reserve\n")); + assert!(asm.contains("call __rt_concat_publish\n")); assert!(asm.contains("mov r11b, BYTE PTR [r8]\n")); assert!(asm.contains("mov rax, QWORD PTR [rbp - 48]\n")); } + + #[test] + /// Verifies that the AArch64 concat path bounds its destination through the shared + /// reservation helpers and rejects a wrapped `left_len + right_len` total. + fn test_emit_concat_aarch64_reserves_bounded_destination() { + let mut emitter = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_concat(&mut emitter); + let asm = emitter.output(); + + assert!(asm.contains("bl __rt_concat_reserve\n")); + assert!(asm.contains("bl __rt_concat_publish\n")); + assert!(asm.contains("adds x5, x2, x4\n")); + assert!(asm.contains("b.cs __rt_concat_size_overflow\n")); + } } diff --git a/src/codegen_support/runtime/strings/concat_scratch.rs b/src/codegen_support/runtime/strings/concat_scratch.rs new file mode 100644 index 0000000000..d03fc35cc3 --- /dev/null +++ b/src/codegen_support/runtime/strings/concat_scratch.rs @@ -0,0 +1,331 @@ +//! Purpose: +//! Emits the shared `__rt_concat_reserve`, `__rt_concat_publish`, and `__rt_alloc_overflow` +//! runtime helpers that bound every append into the fixed 64 KiB `_concat_buf` scratch buffer. +//! Runtime string/IO producers reserve their exact result size here instead of writing past +//! the scratch end into the adjacent BSS globals. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - `__rt_concat_reserve` returns scratch storage when the request still fits inside +//! `_concat_buf`, and an owned heap block (kind word 1 stamped at `[ptr-8]`) otherwise. +//! It never advances `_concat_off`; the caller publishes the *written* length afterwards. +//! - `__rt_concat_publish` derives the storage class from the pointer itself: only pointers +//! inside `[_concat_buf, _concat_buf + 65536)` move `_concat_off`, so heap-backed results +//! leave the shared scratch offset untouched with no extra flag register. +//! - Requests larger than the configured heap capacity (`_heap_max`), including the wrapped +//! or negative sizes produced by an overflowing size computation, terminate through +//! `__rt_alloc_overflow` with PHP's "Possible integer overflow in memory allocation" class +//! of fatal error instead of corrupting memory. +//! - `__rt_alloc_overflow` is a `.globl` fatal trampoline: callers must reach it with an +//! UNCONDITIONAL branch from a local label so macOS atom splitting can never put a +//! conditional branch out of range. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; +use crate::codegen_support::runtime::data::ALLOC_OVERFLOW_MSG; + +/// Byte capacity of the shared `_concat_buf` scratch buffer declared in `runtime::data::fixed`. +pub(crate) const CONCAT_BUF_CAPACITY: usize = 65536; + +/// Uniform heap-header kind stamped on a heap-backed `.` operator result. +/// +/// The `.` operator is the only producer that stamps this kind, and codegen never releases a +/// `StrConcat` value (`value_is_scratch_string` classifies it as transient), so a live block +/// carrying this kind is by construction an unowned temporary with at most one consumer. +/// `__rt_str_persist` uses that to take the block over in place instead of copying it, which +/// is what keeps `$s .= ...` accumulation loops from leaking one oversized block per append. +pub(crate) const CONCAT_TEMP_HEAP_KIND: u32 = 7; + +/// Emits `__rt_concat_reserve`, `__rt_concat_publish`, `__rt_concat_grow`, and +/// `__rt_alloc_overflow`. +/// +/// These helpers form the bounds-checked allocation front end for every runtime +/// producer that used to append blindly at `_concat_buf + _concat_off`. +/// +/// # `__rt_concat_reserve` +/// - Input: `x0` (AArch64) / `rax` (x86_64) = required payload bytes, interpreted as unsigned. +/// - Output: `x0` / `rax` = destination pointer with room for at least that many bytes. +/// - Clobbers every caller-saved register: callers must spill their live state first. +/// - Fatals through `__rt_alloc_overflow` when the request exceeds `_heap_max`. +/// +/// # `__rt_concat_publish` +/// - Input/output: `x1`/`x2` (AArch64) or `rax`/`rdx` (x86_64) = result pointer and length, +/// both preserved so the helper can be dropped straight into a string-returning epilogue. +/// - Advances `_concat_off` only for scratch-backed results; heap-backed results are a no-op. +/// +/// # `__rt_concat_grow` +/// - Input: `x0`/`rax` = current buffer, `x1`/`rdi` = bytes to preserve, `x2`/`rsi` = new capacity. +/// - Output: `x0`/`rax` = larger owned heap buffer holding the preserved prefix. +/// - Releases the superseded buffer through `__rt_heap_free_safe` (a no-op for scratch). +/// +/// # `__rt_alloc_overflow` +/// - Writes PHP's allocation-overflow fatal message to stderr and exits with status 1. +pub fn emit_concat_scratch(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_concat_scratch_linux_x86_64(emitter); + return; + } + + emit_concat_reserve_aarch64(emitter); + emit_concat_publish_aarch64(emitter); + emit_concat_grow_aarch64(emitter); + emit_alloc_overflow_aarch64(emitter); +} + +/// Emits the AArch64 `__rt_concat_reserve` helper. +/// +/// Picks scratch storage while `_concat_off + required` still fits the 64 KiB buffer and +/// falls back to an owned heap allocation otherwise. `_concat_off` is deliberately left +/// unchanged so a caller that writes fewer bytes than it reserved does not waste scratch. +fn emit_concat_reserve_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: concat_reserve ---"); + emitter.label_global("__rt_concat_reserve"); + + // -- reject requests the allocator could never satisfy, including wrapped sizes -- + abi::emit_symbol_address(emitter, "x9", "_heap_max"); + emitter.instruction("ldr x9, [x9]"); // load the configured heap capacity as the upper bound for any single result + emitter.instruction("cmp x0, x9"); // is the requested byte count impossible to satisfy (unsigned, so wrapped sizes are huge)? + emitter.instruction("b.hi __rt_concat_reserve_too_large"); // report a PHP-style allocation overflow instead of writing past any buffer + + // -- prefer the shared 64 KiB scratch buffer while the result still fits -- + abi::emit_symbol_address(emitter, "x10", "_concat_off"); + emitter.instruction("ldr x11, [x10]"); // load the current concat scratch write offset + emitter.instruction("add x12, x11, x0"); // compute the scratch tail this request would reach + emitter.instruction(&format!("mov x13, #{}", CONCAT_BUF_CAPACITY)); // load the concat scratch capacity in bytes + emitter.instruction("cmp x12, x13"); // does the reservation still fit inside the shared scratch buffer? + emitter.instruction("b.hi __rt_concat_reserve_heap"); // use the owned heap fallback when the scratch buffer would overflow + abi::emit_symbol_address(emitter, "x14", "_concat_buf"); + emitter.instruction("add x0, x14, x11"); // return the scratch destination pointer at the current write offset + emitter.instruction("ret"); // return the scratch-backed reservation to the caller + + // -- heap fallback: oversized results get their own owned string allocation -- + emitter.label("__rt_concat_reserve_heap"); + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the frame pointer and return address across the allocator call + emitter.instruction("mov x29, sp"); // establish the reservation helper frame pointer + emitter.instruction("bl __rt_heap_alloc"); // allocate owned storage large enough for the requested result + emitter.instruction("mov x9, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x9, [x0, #-8]"); // stamp the heap allocation as a string payload + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the frame pointer and return address after the allocator call + emitter.instruction("ret"); // return the heap-backed reservation to the caller + + // -- impossible request: report PHP's allocation-overflow fatal error -- + emitter.label("__rt_concat_reserve_too_large"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe +} + +/// Emits the AArch64 `__rt_concat_publish` helper. +/// +/// Classifies the result by address instead of by a caller-supplied flag: only a pointer +/// inside `[_concat_buf, _concat_buf + 65536)` advances `_concat_off`. Heap-backed results +/// own their own storage and must not move the shared scratch cursor. +fn emit_concat_publish_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: concat_publish ---"); + emitter.label_global("__rt_concat_publish"); + + abi::emit_symbol_address(emitter, "x9", "_concat_buf"); + emitter.instruction("sub x10, x1, x9"); // compute the candidate scratch offset of the finished result + emitter.instruction(&format!("mov x11, #{}", CONCAT_BUF_CAPACITY)); // load the concat scratch capacity in bytes + emitter.instruction("cmp x10, x11"); // is the result outside the shared scratch window (unsigned, so heap pointers wrap high)? + emitter.instruction("b.hs __rt_concat_publish_done"); // heap-backed results leave the shared scratch offset untouched + emitter.instruction("add x10, x10, x2"); // advance the scratch offset past the bytes this result actually wrote + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("str x10, [x9]"); // publish the updated concat scratch write offset + + emitter.label("__rt_concat_publish_done"); + emitter.instruction("ret"); // return with the result pointer/length pair untouched +} + +/// Emits the AArch64 `__rt_concat_grow` helper. +/// +/// Moves an in-progress reservation into a larger owned heap block, preserving the bytes +/// written so far. Incremental producers (`stream_get_contents`) call it when the next chunk +/// no longer fits the current reservation. The old block is released through +/// `__rt_heap_free_safe`, which skips concat-scratch and other non-heap pointers. +/// +/// - Input: `x0` = current buffer, `x1` = bytes to preserve, `x2` = new capacity. +/// - Output: `x0` = new buffer. +/// - Clobbers every caller-saved register. +fn emit_concat_grow_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: concat_grow ---"); + emitter.label_global("__rt_concat_grow"); + + emitter.instruction("sub sp, sp, #48"); // allocate spill space for the old buffer, preserved length, and new buffer + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // establish the grow helper frame pointer + emitter.instruction("stp x0, x1, [sp]"); // save the current buffer and the number of bytes to preserve + + // -- allocate the larger owned block and stamp it as an elephc string -- + emitter.instruction("mov x0, x2"); // pass the requested new capacity to the reservation front end + abi::emit_symbol_address(emitter, "x9", "_heap_max"); + emitter.instruction("ldr x9, [x9]"); // load the configured heap capacity as the upper bound for any single result + emitter.instruction("cmp x0, x9"); // is the grown capacity impossible to satisfy? + emitter.instruction("b.hi __rt_concat_grow_too_large"); // report a PHP-style allocation overflow instead of writing past any buffer + emitter.instruction("bl __rt_heap_alloc"); // allocate the larger owned accumulation buffer + emitter.instruction("mov x9, #1"); // heap kind 1 = owned elephc string + emitter.instruction("str x9, [x0, #-8]"); // stamp the heap allocation as a string payload + emitter.instruction("str x0, [sp, #16]"); // save the grown buffer for the return value + + // -- copy the bytes written so far into the grown buffer -- + emitter.instruction("ldp x10, x11, [sp]"); // reload the old buffer pointer and the preserved byte count + emitter.instruction("mov x12, #0"); // byte-copy index + emitter.label("__rt_concat_grow_copy"); + emitter.instruction("cmp x12, x11"); // have all preserved bytes been copied into the grown buffer? + emitter.instruction("b.hs __rt_concat_grow_copy_done"); // leave the copy loop once the preserved prefix is duplicated + emitter.instruction("ldrb w13, [x10, x12]"); // load the next preserved byte from the old buffer + emitter.instruction("strb w13, [x0, x12]"); // store it at the same offset inside the grown buffer + emitter.instruction("add x12, x12, #1"); // advance the copy index + emitter.instruction("b __rt_concat_grow_copy"); // copy the next preserved byte + emitter.label("__rt_concat_grow_copy_done"); + + // -- release the old block when it was heap-backed; scratch pointers are skipped -- + emitter.instruction("ldr x0, [sp]"); // reload the old buffer pointer for release + emitter.instruction("bl __rt_heap_free_safe"); // free the superseded owned block, ignoring concat-scratch pointers + emitter.instruction("ldr x0, [sp, #16]"); // return the grown buffer pointer + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the grow helper frame + emitter.instruction("ret"); // return the grown accumulation buffer + + // -- impossible capacity: report PHP's allocation-overflow fatal error -- + emitter.label("__rt_concat_grow_too_large"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe +} + +/// Emits the AArch64 `__rt_alloc_overflow` fatal trampoline. +/// +/// Mirrors PHP's "Possible integer overflow in memory allocation" fatal: it writes the +/// diagnostic to stderr and exits with a non-zero status rather than faulting. +fn emit_alloc_overflow_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: alloc_overflow (fatal) ---"); + emitter.label_global("__rt_alloc_overflow"); + + emitter.instruction("mov x0, #2"); // fd = stderr for the allocation-overflow diagnostic + abi::emit_symbol_address(emitter, "x1", "_alloc_overflow_msg"); + emitter.instruction(&format!("mov x2, #{}", ALLOC_OVERFLOW_MSG.len())); // pass the exact allocation-overflow diagnostic byte count + emitter.syscall(4); + emitter.instruction("mov x0, #1"); // exit code 1 for the allocation-overflow abort path + emitter.syscall(1); +} + +/// Emits the Linux x86_64 variants of the concat scratch reservation helpers. +/// +/// Same contract as the AArch64 path with System V registers: `rax` carries the requested +/// size into `__rt_concat_reserve` and the destination pointer out, while +/// `__rt_concat_publish` takes and preserves the `rax`/`rdx` string result pair. +fn emit_concat_scratch_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: concat_reserve ---"); + emitter.label_global("__rt_concat_reserve"); + + // -- reject requests the allocator could never satisfy, including wrapped sizes -- + abi::emit_symbol_address(emitter, "r8", "_heap_max"); + emitter.instruction("mov r8, QWORD PTR [r8]"); // load the configured heap capacity as the upper bound for any single result + emitter.instruction("cmp rax, r8"); // is the requested byte count impossible to satisfy (unsigned, so wrapped sizes are huge)? + emitter.instruction("ja __rt_concat_reserve_too_large_x86"); // report a PHP-style allocation overflow instead of writing past any buffer + + // -- prefer the shared 64 KiB scratch buffer while the result still fits -- + abi::emit_symbol_address(emitter, "r9", "_concat_off"); + emitter.instruction("mov r9, QWORD PTR [r9]"); // load the current concat scratch write offset + emitter.instruction("mov r10, r9"); // copy the write offset before deriving the tail this request would reach + emitter.instruction("add r10, rax"); // compute the scratch tail this request would reach + emitter.instruction(&format!("cmp r10, {}", CONCAT_BUF_CAPACITY)); // does the reservation still fit inside the shared scratch buffer? + emitter.instruction("ja __rt_concat_reserve_heap_x86"); // use the owned heap fallback when the scratch buffer would overflow + abi::emit_symbol_address(emitter, "r11", "_concat_buf"); + emitter.instruction("lea rax, [r11 + r9]"); // return the scratch destination pointer at the current write offset + emitter.instruction("ret"); // return the scratch-backed reservation to the caller + + // -- heap fallback: oversized results get their own owned string allocation -- + emitter.label("__rt_concat_reserve_heap_x86"); + emitter.instruction("push rbp"); // preserve the caller frame pointer and realign the stack for the allocator call + emitter.instruction("mov rbp, rsp"); // establish the reservation helper frame pointer + emitter.instruction("call __rt_heap_alloc"); // allocate owned storage large enough for the requested result + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a string payload + emitter.instruction("pop rbp"); // restore the caller frame pointer after the allocator call + emitter.instruction("ret"); // return the heap-backed reservation to the caller + + // -- impossible request: report PHP's allocation-overflow fatal error -- + emitter.label("__rt_concat_reserve_too_large_x86"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller + + emitter.blank(); + emitter.comment("--- runtime: concat_publish ---"); + emitter.label_global("__rt_concat_publish"); + + abi::emit_symbol_address(emitter, "r8", "_concat_buf"); + emitter.instruction("mov r9, rax"); // copy the result pointer before deriving its candidate scratch offset + emitter.instruction("sub r9, r8"); // compute the candidate scratch offset of the finished result + emitter.instruction(&format!("cmp r9, {}", CONCAT_BUF_CAPACITY)); // is the result outside the shared scratch window (unsigned, so heap pointers wrap high)? + emitter.instruction("jae __rt_concat_publish_done_x86"); // heap-backed results leave the shared scratch offset untouched + emitter.instruction("add r9, rdx"); // advance the scratch offset past the bytes this result actually wrote + abi::emit_symbol_address(emitter, "r8", "_concat_off"); + emitter.instruction("mov QWORD PTR [r8], r9"); // publish the updated concat scratch write offset + + emitter.label("__rt_concat_publish_done_x86"); + emitter.instruction("ret"); // return with the result pointer/length pair untouched + + emitter.blank(); + emitter.comment("--- runtime: concat_grow ---"); + emitter.label_global("__rt_concat_grow"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the allocator and release calls + emitter.instruction("mov rbp, rsp"); // establish the grow helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the old buffer, preserved length, and grown buffer + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the current buffer pointer across the allocator call + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the number of bytes to preserve across the allocator call + + // -- allocate the larger owned block and stamp it as an elephc string -- + emitter.instruction("mov rax, rsi"); // pass the requested new capacity to the allocator + abi::emit_symbol_address(emitter, "r8", "_heap_max"); + emitter.instruction("mov r8, QWORD PTR [r8]"); // load the configured heap capacity as the upper bound for any single result + emitter.instruction("cmp rax, r8"); // is the grown capacity impossible to satisfy? + emitter.instruction("ja __rt_concat_grow_too_large_x86"); // report a PHP-style allocation overflow instead of writing past any buffer + emitter.instruction("call __rt_heap_alloc"); // allocate the larger owned accumulation buffer + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a string payload + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the grown buffer for the return value + + // -- copy the bytes written so far into the grown buffer -- + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the old buffer pointer for the preserved-prefix copy + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the preserved byte count for the copy loop + emitter.instruction("xor rcx, rcx"); // byte-copy index + emitter.label("__rt_concat_grow_copy_x86"); + emitter.instruction("cmp rcx, r11"); // have all preserved bytes been copied into the grown buffer? + emitter.instruction("jae __rt_concat_grow_copy_done_x86"); // leave the copy loop once the preserved prefix is duplicated + emitter.instruction("mov r9b, BYTE PTR [r10 + rcx]"); // load the next preserved byte from the old buffer + emitter.instruction("mov BYTE PTR [rax + rcx], r9b"); // store it at the same offset inside the grown buffer + emitter.instruction("inc rcx"); // advance the copy index + emitter.instruction("jmp __rt_concat_grow_copy_x86"); // copy the next preserved byte + emitter.label("__rt_concat_grow_copy_done_x86"); + + // -- release the old block when it was heap-backed; scratch pointers are skipped -- + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the old buffer pointer for release + emitter.instruction("call __rt_heap_free_safe"); // free the superseded owned block, ignoring concat-scratch pointers + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the grown buffer pointer + emitter.instruction("add rsp, 32"); // release the grow helper spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the grown accumulation buffer + + // -- impossible capacity: report PHP's allocation-overflow fatal error -- + emitter.label("__rt_concat_grow_too_large_x86"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller + + emitter.blank(); + emitter.comment("--- runtime: alloc_overflow (fatal) ---"); + emitter.label_global("__rt_alloc_overflow"); + + emitter.instruction("mov edi, 2"); // fd = stderr for the allocation-overflow diagnostic + abi::emit_symbol_address(emitter, "rsi", "_alloc_overflow_msg"); + emitter.instruction(&format!("mov edx, {}", ALLOC_OVERFLOW_MSG.len())); // pass the exact allocation-overflow diagnostic byte count + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // emit the fatal allocation-overflow message before terminating + emitter.instruction("mov edi, 1"); // exit code 1 for the allocation-overflow abort path + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the impossible allocation +} diff --git a/src/codegen_support/runtime/strings/count_chars.rs b/src/codegen_support/runtime/strings/count_chars.rs new file mode 100644 index 0000000000..b20ede92d7 --- /dev/null +++ b/src/codegen_support/runtime/strings/count_chars.rs @@ -0,0 +1,353 @@ +//! Purpose: +//! Emits the `__rt_count_chars` runtime helper assembly for PHP's `count_chars`: tallies every +//! byte value of the subject and materializes the shape the requested `$mode` selects. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The 256-entry tally lives on the helper's own frame, so the byte counting pass never +//! touches shared runtime state. +//! - Modes 0, 1, and 2 build a hash with integer keys and integer values: mode 0 emits every +//! byte value, mode 1 only the used ones, and mode 2 only the unused ones. Insertion runs +//! from byte 0 upwards, which is the key order php-src produces. +//! - Modes 3 and 4 render the used / unused byte values as a string. The result is reserved +//! through `__rt_concat_reserve` (never more than 256 bytes, but the bound is enforced the +//! same way as for every other producer) and then copied into owned heap storage by +//! `__rt_str_persist`, which is what the `Fresh` ownership contract on +//! `RuntimeFnId::CountChars` promises for both result shapes. The reservation itself is +//! then released through `__rt_heap_free_safe`, which is a no-op for the scratch-backed +//! case and prevents a leak when the shared scratch was already nearly full. +//! - A mode outside `0..=4` never reaches this helper: the EIR lowering raises php-src's +//! catchable `ValueError` before the call. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_count_chars` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = subject pointer, `x2` = subject length, `x3` = mode (0..=4). +/// Output: `x0` = tally hash pointer for modes 0-2; `x1`/`x2` = byte-list string pair for +/// modes 3-4. +/// +/// ABI (x86_64 System V): +/// Input: `rax` = subject pointer, `rdx` = subject length, `rdi` = mode (0..=4). +/// Output: `rax` = tally hash pointer for modes 0-2; `rax`/`rdx` = string pair for modes 3-4. +/// +/// Clobbers every caller-saved register: the result paths reach `__rt_hash_new`, +/// `__rt_hash_set`, `__rt_concat_reserve`, `__rt_concat_publish`, and `__rt_str_persist`. +pub fn emit_count_chars(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_count_chars_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: count_chars ---"); + emitter.label_global("__rt_count_chars"); + + // Frame layout (2128 bytes). The saved register pair sits at offset 0 because the + // 2 KiB tally would push it far outside `stp`'s scaled immediate range. + // [sp, #0] = saved x29/x30 + // [sp, #16] = subject pointer + // [sp, #24] = subject length + // [sp, #32] = requested mode + // [sp, #40] = running result (hash pointer or reserved string start) + // [sp, #48] = result byte count for the string modes + // [sp, #56] = byte-value loop index + // [sp, #80] = 256-entry byte tally + emitter.instruction("sub sp, sp, #2128"); // allocate the tally frame + emitter.instruction("stp x29, x30, [sp]"); // save the frame pointer and return address across the result helper calls + emitter.instruction("mov x29, sp"); // establish the count_chars helper frame pointer + emitter.instruction("str x1, [sp, #16]"); // save the subject pointer for the counting pass + emitter.instruction("str x2, [sp, #24]"); // save the subject length for the counting pass + emitter.instruction("str x3, [sp, #32]"); // save the requested result mode + + // -- clear the 256-entry byte tally -- + emitter.instruction("add x10, sp, #80"); // x10 = tally base, recomputed after every helper call + emitter.instruction("mov x9, #0"); // start clearing at byte value zero + emitter.label("__rt_count_chars_zero"); + emitter.instruction("str xzr, [x10, x9, lsl #3]"); // clear one byte-value tally + emitter.instruction("add x9, x9, #1"); // advance to the next byte value + emitter.instruction("cmp x9, #256"); // has the whole tally been cleared? + emitter.instruction("b.lo __rt_count_chars_zero"); // keep clearing until every byte value is zeroed + + // -- tally every subject byte -- + emitter.instruction("mov x9, #0"); // start at the first subject byte + emitter.label("__rt_count_chars_count"); + emitter.instruction("cmp x9, x2"); // has the whole subject been counted? + emitter.instruction("b.hs __rt_count_chars_count_done"); // the tally is complete + emitter.instruction("ldrb w11, [x1, x9]"); // load the next subject byte + emitter.instruction("ldr x12, [x10, x11, lsl #3]"); // load that byte value's running tally + emitter.instruction("add x12, x12, #1"); // count one more occurrence + emitter.instruction("str x12, [x10, x11, lsl #3]"); // publish the updated tally + emitter.instruction("add x9, x9, #1"); // advance to the next subject byte + emitter.instruction("b __rt_count_chars_count"); // keep counting subject bytes + emitter.label("__rt_count_chars_count_done"); + + emitter.instruction("ldr x3, [sp, #32]"); // reload the requested result mode + emitter.instruction("cmp x3, #3"); // do modes 3 and 4 want the byte-list string? + emitter.instruction("b.ge __rt_count_chars_string"); // render the byte list instead of a tally + + // -- modes 0, 1, and 2 build the integer-keyed tally hash -- + emitter.instruction("mov x0, #16"); // seed the tally hash with a small capacity + emitter.instruction("mov x1, xzr"); // value_type 0 = integer values + emitter.instruction("bl __rt_hash_new"); // allocate the tally hash + emitter.instruction("str x0, [sp, #40]"); // publish the tally hash pointer + emitter.instruction("str xzr, [sp, #56]"); // start emitting at byte value zero + + emitter.label("__rt_count_chars_array"); + emitter.instruction("ldr x9, [sp, #56]"); // reload the byte-value loop index + emitter.instruction("cmp x9, #256"); // have all byte values been considered? + emitter.instruction("b.hs __rt_count_chars_array_done"); // the tally hash is complete + emitter.instruction("add x10, sp, #80"); // restore the tally base after the previous helper call + emitter.instruction("ldr x12, [x10, x9, lsl #3]"); // load this byte value's tally + emitter.instruction("ldr x3, [sp, #32]"); // reload the requested result mode + emitter.instruction("cbz x3, __rt_count_chars_array_emit"); // mode 0 emits every byte value + emitter.instruction("cmp x3, #1"); // is the caller asking for the used byte values only? + emitter.instruction("b.ne __rt_count_chars_array_unused"); // mode 2 keeps the unused byte values instead + emitter.instruction("cbz x12, __rt_count_chars_array_next"); // mode 1 skips byte values the subject never uses + emitter.instruction("b __rt_count_chars_array_emit"); // a used byte value is emitted with its tally + emitter.label("__rt_count_chars_array_unused"); + emitter.instruction("cbnz x12, __rt_count_chars_array_next"); // mode 2 skips byte values the subject does use + + emitter.label("__rt_count_chars_array_emit"); + emitter.instruction("ldr x0, [sp, #40]"); // reload the tally hash pointer + emitter.instruction("mov x1, x9"); // key_lo = the byte value + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer key + emitter.instruction("mov x3, x12"); // value_lo = the occurrence tally + emitter.instruction("mov x4, xzr"); // integer tallies carry no high word + emitter.instruction("mov x5, xzr"); // runtime tag 0 marks the tally as an int + emitter.instruction("bl __rt_hash_set"); // insert the byte value's tally + emitter.instruction("str x0, [sp, #40]"); // republish the hash pointer after possible growth + + emitter.label("__rt_count_chars_array_next"); + emitter.instruction("ldr x9, [sp, #56]"); // reload the byte-value loop index + emitter.instruction("add x9, x9, #1"); // advance to the next byte value + emitter.instruction("str x9, [sp, #56]"); // publish the advanced loop index + emitter.instruction("b __rt_count_chars_array"); // consider the next byte value + + emitter.label("__rt_count_chars_array_done"); + emitter.instruction("ldr x0, [sp, #40]"); // return the finished tally hash + emitter.instruction("ldp x29, x30, [sp]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #2128"); // release the tally frame + emitter.instruction("ret"); // return the tally as a PHP array + + // -- modes 3 and 4 render the selected byte values as a string -- + emitter.label("__rt_count_chars_string"); + emitter.instruction("add x10, sp, #80"); // x10 = tally base for the selection pass + emitter.instruction("mov x9, #0"); // start at byte value zero + emitter.instruction("mov x13, #0"); // running count of selected byte values + + emitter.label("__rt_count_chars_select"); + emitter.instruction("cmp x9, #256"); // have all byte values been considered? + emitter.instruction("b.hs __rt_count_chars_select_done"); // the exact result size is known + emitter.instruction("ldr x12, [x10, x9, lsl #3]"); // load this byte value's tally + emitter.instruction("cmp x3, #3"); // is the caller asking for the used byte values? + emitter.instruction("b.ne __rt_count_chars_select_unused"); // mode 4 selects the unused byte values instead + emitter.instruction("cbz x12, __rt_count_chars_select_next"); // mode 3 skips byte values the subject never uses + emitter.instruction("b __rt_count_chars_select_hit"); // a used byte value joins the result + emitter.label("__rt_count_chars_select_unused"); + emitter.instruction("cbnz x12, __rt_count_chars_select_next"); // mode 4 skips byte values the subject does use + emitter.label("__rt_count_chars_select_hit"); + emitter.instruction("add x13, x13, #1"); // reserve one more result byte + emitter.label("__rt_count_chars_select_next"); + emitter.instruction("add x9, x9, #1"); // advance to the next byte value + emitter.instruction("b __rt_count_chars_select"); // keep sizing the result + + emitter.label("__rt_count_chars_select_done"); + emitter.instruction("str x13, [sp, #48]"); // save the exact result byte count + emitter.instruction("mov x0, x13"); // request exactly that many bytes + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the byte list + emitter.instruction("str x0, [sp, #40]"); // save the reserved result start + emitter.instruction("mov x14, x0"); // destination cursor + emitter.instruction("add x10, sp, #80"); // restore the tally base after the reservation call + emitter.instruction("ldr x3, [sp, #32]"); // reload the requested result mode + emitter.instruction("mov x9, #0"); // start at byte value zero + + emitter.label("__rt_count_chars_fill"); + emitter.instruction("cmp x9, #256"); // have all byte values been considered? + emitter.instruction("b.hs __rt_count_chars_fill_done"); // the byte list is complete + emitter.instruction("ldr x12, [x10, x9, lsl #3]"); // load this byte value's tally + emitter.instruction("cmp x3, #3"); // is the caller asking for the used byte values? + emitter.instruction("b.ne __rt_count_chars_fill_unused"); // mode 4 writes the unused byte values instead + emitter.instruction("cbz x12, __rt_count_chars_fill_next"); // mode 3 skips byte values the subject never uses + emitter.instruction("b __rt_count_chars_fill_hit"); // a used byte value joins the result + emitter.label("__rt_count_chars_fill_unused"); + emitter.instruction("cbnz x12, __rt_count_chars_fill_next"); // mode 4 skips byte values the subject does use + emitter.label("__rt_count_chars_fill_hit"); + emitter.instruction("strb w9, [x14], #1"); // append the selected byte value to the result + emitter.label("__rt_count_chars_fill_next"); + emitter.instruction("add x9, x9, #1"); // advance to the next byte value + emitter.instruction("b __rt_count_chars_fill"); // keep filling the byte list + + emitter.label("__rt_count_chars_fill_done"); + emitter.instruction("ldr x1, [sp, #40]"); // the byte list starts at the reserved pointer + emitter.instruction("ldr x2, [sp, #48]"); // the byte list is exactly as long as it was sized + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("bl __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("str x1, [sp, #56]"); // save the owned byte-list pointer across the reservation release + emitter.instruction("str x2, [sp, #64]"); // save the owned byte-list length across the reservation release + emitter.instruction("ldr x0, [sp, #40]"); // reload the superseded reservation + emitter.instruction("bl __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("ldr x1, [sp, #56]"); // restore the owned byte-list pointer + emitter.instruction("ldr x2, [sp, #64]"); // restore the owned byte-list length + emitter.instruction("ldp x29, x30, [sp]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #2128"); // release the tally frame + emitter.instruction("ret"); // return the byte list as a PHP string pair +} + +/// Emits `__rt_count_chars` for x86_64 Linux using the System V ABI. +/// +/// The 256-entry tally is addressed as `[rbp + index*8 - 2128]`, so no register has to +/// survive the result helper calls to keep it reachable. The saved-value slots start at +/// `[rbp-32]` to stay clear of the `[rbp-8]`..`[rbp-24]` window other runtime emitters +/// reserve for pushed callee-saved registers. +fn emit_count_chars_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: count_chars ---"); + emitter.label_global("__rt_count_chars"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the result helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the tally and saved arguments + emitter.instruction("sub rsp, 2128"); // reserve the saved-argument slots plus the 2 KiB byte tally + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the subject pointer for the counting pass + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the subject length for the counting pass + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the requested result mode + + // -- clear the 256-entry byte tally -- + emitter.instruction("xor r9d, r9d"); // start clearing at byte value zero + emitter.label("__rt_count_chars_zero_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp + r9*8 - 2128], 0"); // clear one byte-value tally + emitter.instruction("add r9, 1"); // advance to the next byte value + emitter.instruction("cmp r9, 256"); // has the whole tally been cleared? + emitter.instruction("jb __rt_count_chars_zero_linux_x86_64"); // keep clearing until every byte value is zeroed + + // -- tally every subject byte -- + emitter.instruction("xor r9d, r9d"); // start at the first subject byte + emitter.label("__rt_count_chars_count_linux_x86_64"); + emitter.instruction("cmp r9, rdx"); // has the whole subject been counted? + emitter.instruction("jae __rt_count_chars_count_done_linux_x86_64"); // the tally is complete + emitter.instruction("movzx r10d, BYTE PTR [rax + r9]"); // load the next subject byte + emitter.instruction("add QWORD PTR [rbp + r10*8 - 2128], 1"); // count one more occurrence of that byte value + emitter.instruction("add r9, 1"); // advance to the next subject byte + emitter.instruction("jmp __rt_count_chars_count_linux_x86_64"); // keep counting subject bytes + emitter.label("__rt_count_chars_count_done_linux_x86_64"); + + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the requested result mode + emitter.instruction("cmp r11, 3"); // do modes 3 and 4 want the byte-list string? + emitter.instruction("jge __rt_count_chars_string_linux_x86_64"); // render the byte list instead of a tally + + // -- modes 0, 1, and 2 build the integer-keyed tally hash -- + emitter.instruction("mov edi, 16"); // seed the tally hash with a small capacity + emitter.instruction("xor esi, esi"); // value_type 0 = integer values + emitter.instruction("call __rt_hash_new"); // allocate the tally hash + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // publish the tally hash pointer + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // start emitting at byte value zero + + emitter.label("__rt_count_chars_array_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 72]"); // reload the byte-value loop index + emitter.instruction("cmp r9, 256"); // have all byte values been considered? + emitter.instruction("jae __rt_count_chars_array_done_linux_x86_64"); // the tally hash is complete + emitter.instruction("mov r10, QWORD PTR [rbp + r9*8 - 2128]"); // load this byte value's tally + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the requested result mode + emitter.instruction("test r11, r11"); // is the caller asking for every byte value? + emitter.instruction("jz __rt_count_chars_array_emit_linux_x86_64"); // mode 0 emits every byte value + emitter.instruction("cmp r11, 1"); // is the caller asking for the used byte values only? + emitter.instruction("jne __rt_count_chars_array_unused_linux_x86_64"); // mode 2 keeps the unused byte values instead + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jz __rt_count_chars_array_next_linux_x86_64"); // mode 1 skips byte values the subject never uses + emitter.instruction("jmp __rt_count_chars_array_emit_linux_x86_64"); // a used byte value is emitted with its tally + emitter.label("__rt_count_chars_array_unused_linux_x86_64"); + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jnz __rt_count_chars_array_next_linux_x86_64"); // mode 2 skips byte values the subject does use + + emitter.label("__rt_count_chars_array_emit_linux_x86_64"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the tally hash pointer + emitter.instruction("mov rsi, r9"); // key_lo = the byte value + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer key + emitter.instruction("mov rcx, r10"); // value_lo = the occurrence tally + emitter.instruction("xor r8d, r8d"); // integer tallies carry no high word + emitter.instruction("xor r9d, r9d"); // runtime tag 0 marks the tally as an int + emitter.instruction("call __rt_hash_set"); // insert the byte value's tally + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // republish the hash pointer after possible growth + + emitter.label("__rt_count_chars_array_next_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 72]"); // reload the byte-value loop index + emitter.instruction("add r9, 1"); // advance to the next byte value + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // publish the advanced loop index + emitter.instruction("jmp __rt_count_chars_array_linux_x86_64"); // consider the next byte value + + emitter.label("__rt_count_chars_array_done_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the finished tally hash + emitter.instruction("add rsp, 2128"); // release the tally frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the tally as a PHP array + + // -- modes 3 and 4 render the selected byte values as a string -- + emitter.label("__rt_count_chars_string_linux_x86_64"); + emitter.instruction("xor r9d, r9d"); // start at byte value zero + emitter.instruction("xor ecx, ecx"); // running count of selected byte values + + emitter.label("__rt_count_chars_select_linux_x86_64"); + emitter.instruction("cmp r9, 256"); // have all byte values been considered? + emitter.instruction("jae __rt_count_chars_select_done_linux_x86_64"); // the exact result size is known + emitter.instruction("mov r10, QWORD PTR [rbp + r9*8 - 2128]"); // load this byte value's tally + emitter.instruction("cmp r11, 3"); // is the caller asking for the used byte values? + emitter.instruction("jne __rt_count_chars_select_unused_linux_x86_64"); // mode 4 selects the unused byte values instead + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jz __rt_count_chars_select_next_linux_x86_64"); // mode 3 skips byte values the subject never uses + emitter.instruction("jmp __rt_count_chars_select_hit_linux_x86_64"); // a used byte value joins the result + emitter.label("__rt_count_chars_select_unused_linux_x86_64"); + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jnz __rt_count_chars_select_next_linux_x86_64"); // mode 4 skips byte values the subject does use + emitter.label("__rt_count_chars_select_hit_linux_x86_64"); + emitter.instruction("add rcx, 1"); // reserve one more result byte + emitter.label("__rt_count_chars_select_next_linux_x86_64"); + emitter.instruction("add r9, 1"); // advance to the next byte value + emitter.instruction("jmp __rt_count_chars_select_linux_x86_64"); // keep sizing the result + + emitter.label("__rt_count_chars_select_done_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // save the exact result byte count + emitter.instruction("mov rax, rcx"); // request exactly that many bytes + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the byte list + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the reserved result start + emitter.instruction("mov rsi, rax"); // destination cursor + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the requested result mode + emitter.instruction("xor r9d, r9d"); // start at byte value zero + + emitter.label("__rt_count_chars_fill_linux_x86_64"); + emitter.instruction("cmp r9, 256"); // have all byte values been considered? + emitter.instruction("jae __rt_count_chars_fill_done_linux_x86_64"); // the byte list is complete + emitter.instruction("mov r10, QWORD PTR [rbp + r9*8 - 2128]"); // load this byte value's tally + emitter.instruction("cmp r11, 3"); // is the caller asking for the used byte values? + emitter.instruction("jne __rt_count_chars_fill_unused_linux_x86_64"); // mode 4 writes the unused byte values instead + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jz __rt_count_chars_fill_next_linux_x86_64"); // mode 3 skips byte values the subject never uses + emitter.instruction("jmp __rt_count_chars_fill_hit_linux_x86_64"); // a used byte value joins the result + emitter.label("__rt_count_chars_fill_unused_linux_x86_64"); + emitter.instruction("test r10, r10"); // did the subject use this byte value? + emitter.instruction("jnz __rt_count_chars_fill_next_linux_x86_64"); // mode 4 skips byte values the subject does use + emitter.label("__rt_count_chars_fill_hit_linux_x86_64"); + emitter.instruction("mov BYTE PTR [rsi], r9b"); // append the selected byte value to the result + emitter.instruction("add rsi, 1"); // advance the destination cursor + emitter.label("__rt_count_chars_fill_next_linux_x86_64"); + emitter.instruction("add r9, 1"); // advance to the next byte value + emitter.instruction("jmp __rt_count_chars_fill_linux_x86_64"); // keep filling the byte list + + emitter.label("__rt_count_chars_fill_done_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // the byte list starts at the reserved pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // the byte list is exactly as long as it was sized + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("call __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the owned byte-list pointer across the reservation release + emitter.instruction("mov QWORD PTR [rbp - 80], rdx"); // save the owned byte-list length across the reservation release + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the superseded reservation + emitter.instruction("call __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // restore the owned byte-list pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // restore the owned byte-list length + emitter.instruction("add rsp, 2128"); // release the tally frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the byte list as a PHP string pair +} diff --git a/src/codegen_support/runtime/strings/dec_to_base.rs b/src/codegen_support/runtime/strings/dec_to_base.rs new file mode 100644 index 0000000000..7e23bac0b3 --- /dev/null +++ b/src/codegen_support/runtime/strings/dec_to_base.rs @@ -0,0 +1,158 @@ +//! Purpose: +//! Emits the `__rt_dec_to_base` runtime helper assembly shared by PHP's `dechex`, `decbin`, +//! and `decoct` builtins: renders a 64-bit value as an unsigned string in a given base. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The value is interpreted as UNSIGNED, which is what makes `dechex(-1)` render +//! `"ffffffffffffffff"` exactly like reference PHP instead of a signed `-1`. +//! - Digits above 9 use lowercase `a`-`z`, matching php-src's `_php_math_longtobase`. +//! - Digits are produced least-significant-first into a frame-local 64-byte buffer (the widest +//! possible result is base 2 of `PHP_INT_MAX`-sized input, i.e. 64 characters) and only then +//! copied into a reservation of the EXACT result size taken from `__rt_concat_reserve`. +//! Writing right-to-left straight into the reservation would hand back an interior pointer +//! of a heap-backed block, which the release path could not free. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_dec_to_base` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x0` = value (read as unsigned 64-bit), `x3` = base (2..36). +/// Output: `x1` = result pointer, `x2` = result length. +/// +/// ABI (x86_64 System V): +/// Input: `rax` = value (read as unsigned 64-bit), `rdi` = base (2..36). +/// Output: `rax` = result pointer, `rdx` = result length. +/// +/// A zero value renders the single character `"0"`; every other value renders without +/// leading zeros. The result is published through `__rt_concat_publish`, so it lives in the +/// shared concat scratch while it fits and in an owned heap block otherwise. +pub fn emit_dec_to_base(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_dec_to_base_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: dec_to_base ---"); + emitter.label_global("__rt_dec_to_base"); + + emitter.instruction("sub sp, sp, #96"); // reserve a 64-byte digit buffer plus spill and frame slots + emitter.instruction("stp x29, x30, [sp, #80]"); // save the frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #80"); // establish the dec_to_base helper frame pointer + emitter.instruction("add x9, sp, #64"); // point the digit cursor just past the end of the digit buffer + emitter.instruction("mov x10, #0"); // start with no digits emitted + emitter.instruction("mov x11, x0"); // copy the value into the shrinking conversion accumulator + emitter.instruction("mov x12, x3"); // keep the requested base in a stable register + emitter.instruction("cbnz x11, __rt_dec_to_base_loop"); // a non-zero value produces its digits in the loop below + emitter.instruction("mov w13, #48"); // ASCII '0' is the whole result for a zero value + emitter.instruction("strb w13, [x9, #-1]!"); // write the single zero digit and step the cursor back onto it + emitter.instruction("mov x10, #1"); // the zero result is exactly one character long + emitter.instruction("b __rt_dec_to_base_emit"); // publish the single-character result + + emitter.label("__rt_dec_to_base_loop"); + emitter.instruction("udiv x14, x11, x12"); // divide the accumulator by the base, unsigned + emitter.instruction("msub x15, x14, x12, x11"); // recover the remainder as value - quotient * base + emitter.instruction("mov x11, x14"); // the quotient becomes the next accumulator + emitter.instruction("cmp x15, #10"); // does this digit need a letter rather than a numeral? + emitter.instruction("b.lo __rt_dec_to_base_numeral"); // digits 0-9 use the ASCII numerals + emitter.instruction("add w15, w15, #87"); // map digits 10-35 to lowercase 'a'-'z' + emitter.instruction("b __rt_dec_to_base_store"); // the digit character is ready to store + emitter.label("__rt_dec_to_base_numeral"); + emitter.instruction("add w15, w15, #48"); // map digits 0-9 to ASCII '0'-'9' + emitter.label("__rt_dec_to_base_store"); + emitter.instruction("strb w15, [x9, #-1]!"); // store the digit least-significant-first, walking backwards + emitter.instruction("add x10, x10, #1"); // count the digit just emitted + emitter.instruction("cbnz x11, __rt_dec_to_base_loop"); // keep converting while the accumulator is non-zero + + emitter.label("__rt_dec_to_base_emit"); + emitter.instruction("stp x9, x10, [sp, #64]"); // save the first-digit pointer and the digit count across the reservation + emitter.instruction("mov x0, x10"); // reserve exactly as many bytes as the rendered result needs + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the rendered digits + emitter.instruction("ldp x9, x10, [sp, #64]"); // reload the first-digit pointer and digit count after the reservation + emitter.instruction("mov x1, x0"); // the reservation start is the published result pointer + emitter.instruction("mov x2, x10"); // the digit count is the published result length + emitter.instruction("mov x13, #0"); // start copying at the first rendered digit + + emitter.label("__rt_dec_to_base_copy"); + emitter.instruction("cmp x13, x10"); // have all rendered digits been copied out? + emitter.instruction("b.hs __rt_dec_to_base_copied"); // finish once the whole result has been copied + emitter.instruction("ldrb w14, [x9, x13]"); // load the next rendered digit from the frame buffer + emitter.instruction("strb w14, [x0, x13]"); // store it into the reserved result storage + emitter.instruction("add x13, x13, #1"); // advance the copy index + emitter.instruction("b __rt_dec_to_base_copy"); // copy the next rendered digit + + emitter.label("__rt_dec_to_base_copied"); + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the dec_to_base helper frame + emitter.instruction("ret"); // return the rendered digits as a PHP string pair +} + +/// Emits `__rt_dec_to_base` for x86_64 Linux using the System V ABI. +/// +/// The frame keeps its 64-byte digit buffer at `[rbp-128, rbp-64)` and its two spill slots at +/// `[rbp-64]`/`[rbp-56]`, deliberately clear of `[rbp-8]`..`[rbp-24]` so the layout cannot +/// collide with the saved-register slots other runtime emitters expect there. +fn emit_dec_to_base_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: dec_to_base ---"); + emitter.label_global("__rt_dec_to_base"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish the dec_to_base helper frame pointer + emitter.instruction("sub rsp, 128"); // reserve the digit buffer and spill slots, keeping the stack 16-byte aligned + emitter.instruction("lea r9, [rbp - 64]"); // point the digit cursor just past the end of the digit buffer + emitter.instruction("xor r10d, r10d"); // start with no digits emitted + emitter.instruction("mov r11, rdi"); // keep the requested base in a stable register + emitter.instruction("test rax, rax"); // is the value zero? + emitter.instruction("jnz __rt_dec_to_base_loop_linux_x86_64"); // a non-zero value produces its digits in the loop below + emitter.instruction("sub r9, 1"); // step the cursor back onto the single zero digit + emitter.instruction("mov BYTE PTR [r9], 48"); // ASCII '0' is the whole result for a zero value + emitter.instruction("mov r10, 1"); // the zero result is exactly one character long + emitter.instruction("jmp __rt_dec_to_base_emit_linux_x86_64"); // publish the single-character result + + emitter.label("__rt_dec_to_base_loop_linux_x86_64"); + emitter.instruction("xor edx, edx"); // clear the high dividend half before the unsigned division + emitter.instruction("div r11"); // divide the accumulator by the base, leaving the remainder in rdx + emitter.instruction("cmp rdx, 10"); // does this digit need a letter rather than a numeral? + emitter.instruction("jb __rt_dec_to_base_numeral_linux_x86_64"); // digits 0-9 use the ASCII numerals + emitter.instruction("add rdx, 87"); // map digits 10-35 to lowercase 'a'-'z' + emitter.instruction("jmp __rt_dec_to_base_store_linux_x86_64"); // the digit character is ready to store + emitter.label("__rt_dec_to_base_numeral_linux_x86_64"); + emitter.instruction("add rdx, 48"); // map digits 0-9 to ASCII '0'-'9' + emitter.label("__rt_dec_to_base_store_linux_x86_64"); + emitter.instruction("sub r9, 1"); // walk the digit cursor backwards by one character + emitter.instruction("mov BYTE PTR [r9], dl"); // store the digit least-significant-first + emitter.instruction("add r10, 1"); // count the digit just emitted + emitter.instruction("test rax, rax"); // is the remaining quotient non-zero? + emitter.instruction("jnz __rt_dec_to_base_loop_linux_x86_64"); // keep converting while the accumulator is non-zero + + emitter.label("__rt_dec_to_base_emit_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // save the first-digit pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the digit count across the reservation call + emitter.instruction("mov rax, r10"); // reserve exactly as many bytes as the rendered result needs + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the rendered digits + emitter.instruction("mov r9, QWORD PTR [rbp - 64]"); // reload the first-digit pointer after the reservation + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the digit count after the reservation + emitter.instruction("xor ecx, ecx"); // start copying at the first rendered digit + + emitter.label("__rt_dec_to_base_copy_linux_x86_64"); + emitter.instruction("cmp rcx, r10"); // have all rendered digits been copied out? + emitter.instruction("jae __rt_dec_to_base_copied_linux_x86_64"); // finish once the whole result has been copied + emitter.instruction("mov r8b, BYTE PTR [r9 + rcx]"); // load the next rendered digit from the frame buffer + emitter.instruction("mov BYTE PTR [rax + rcx], r8b"); // store it into the reserved result storage + emitter.instruction("add rcx, 1"); // advance the copy index + emitter.instruction("jmp __rt_dec_to_base_copy_linux_x86_64"); // copy the next rendered digit + + emitter.label("__rt_dec_to_base_copied_linux_x86_64"); + emitter.instruction("mov rdx, r10"); // the digit count is the published result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 128"); // release the dec_to_base helper frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the rendered digits as a PHP string pair +} diff --git a/src/codegen_support/runtime/strings/explode.rs b/src/codegen_support/runtime/strings/explode.rs index b4a644fb62..ebee5a9c0b 100644 --- a/src/codegen_support/runtime/strings/explode.rs +++ b/src/codegen_support/runtime/strings/explode.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits the `__rt_explode`, `__rt_array_new` runtime helper assembly for explode. +//! Emits the `__rt_explode` runtime helper assembly for PHP's `explode()`. //! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. //! //! Called from: @@ -7,18 +7,24 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - The helper implements PHP's full `$limit` contract: a positive limit caps the element +//! count and lets the last element absorb the remaining suffix, `0` behaves like `1`, and a +//! negative limit drops that many trailing segments. Negative limits therefore need the +//! segment total up front, which is why the delimiter scan is factored into a local +//! subroutine shared by a counting pass and the emitting pass. +//! - A zero-length separator returns "no match" instead of matching everywhere. Reference PHP +//! raises `ValueError` for it and the EIR lowering guard does the same before this helper is +//! reached; the check here only keeps the scan from looping forever if it ever is. use crate::codegen_support::{emit::Emitter, platform::Arch}; /// Emits the `__rt_explode` runtime helper for splitting a string by a delimiter. /// -/// Dispatches to `emit_explode_linux_x86_64` on x86_64 Linux; falls through to the ARM64 +/// Dispatches to `emit_explode_linux_x86_64` on x86_64; falls through to the ARM64 /// implementation on all other targets. Uses target ABI registers for the pointer/length -/// pairs: x1/x2 = delimiter ptr/length, x3/x4 = subject string ptr/length, x0 = result -/// array pointer. Allocates an initial indexed array with 16 string slots and pushes each -/// extracted segment via `__rt_array_push_str`. The final segment (after the last delimiter -/// or the entire string if no delimiter is found) is always pushed. Stack frame is 80 bytes -/// on ARM64; red-zone frame is 64 bytes on x86_64 Linux. +/// pairs: x1/x2 = delimiter ptr/length, x3/x4 = subject ptr/length, x5 = `$limit`, x0 = +/// result array pointer. Allocates an initial indexed array with 16 string slots and pushes +/// each retained segment via `__rt_array_push_str`. Stack frame is 112 bytes on ARM64. pub fn emit_explode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_explode_linux_x86_64(emitter); @@ -29,12 +35,20 @@ pub fn emit_explode(emitter: &mut Emitter) { emitter.comment("--- runtime: explode ---"); emitter.label_global("__rt_explode"); - // -- set up stack frame (80 bytes) -- - emitter.instruction("sub sp, sp, #80"); // allocate 80 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #64"); // establish new frame pointer + // -- set up stack frame (112 bytes) -- + // [sp+0] delimiter ptr [sp+8] delimiter len + // [sp+16] subject ptr [sp+24] subject len + // [sp+32] result array [sp+40] scan position + // [sp+48] segment start [sp+56] element cap + // [sp+64] extend-last [sp+72] emitted count + // [sp+80] segment total [sp+88] delimiter-scan start + // [sp+96] saved x29, x30 + emitter.instruction("sub sp, sp, #112"); // allocate the explode() scan frame + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #96"); // establish new frame pointer emitter.instruction("stp x1, x2, [sp]"); // save delimiter ptr and length - emitter.instruction("stp x3, x4, [sp, #16]"); // save input string ptr and length + emitter.instruction("stp x3, x4, [sp, #16]"); // save subject ptr and length + emitter.instruction("str x5, [sp, #56]"); // save the raw PHP $limit before it becomes an element cap // -- create a new string array -- emitter.instruction("mov x0, #16"); // initial array capacity = 16 elements @@ -42,86 +56,149 @@ pub fn emit_explode(emitter: &mut Emitter) { emitter.instruction("bl __rt_array_new"); // call array constructor, returns array in x0 emitter.instruction("str x0, [sp, #32]"); // save array pointer on stack - // -- initialize scan state -- - emitter.instruction("mov x13, #0"); // current scan position = 0 - emitter.instruction("str x13, [sp, #40]"); // save current scan position - emitter.instruction("str x13, [sp, #48]"); // segment start = 0 + // -- translate PHP's $limit into an element cap plus a last-element rule -- + emitter.instruction("ldr x9, [sp, #56]"); // reload the raw $limit + emitter.instruction("cmp x9, #0"); // classify the limit as positive, zero, or negative + emitter.instruction("b.gt __rt_explode_cap_positive"); // a positive limit is already the element cap + emitter.instruction("b.lt __rt_explode_cap_negative"); // a negative limit drops that many trailing segments + emitter.instruction("mov x9, #1"); // PHP treats $limit === 0 exactly like $limit === 1 + emitter.label("__rt_explode_cap_positive"); + emitter.instruction("str x9, [sp, #56]"); // publish the element cap + emitter.instruction("mov x10, #1"); // positive limits let the final element absorb the rest of the subject + emitter.instruction("str x10, [sp, #64]"); // publish the extend-last-element rule + emitter.instruction("b __rt_explode_scan_init"); // start emitting segments + + // -- negative limit: count the segments first so the cap can drop the tail -- + emitter.label("__rt_explode_cap_negative"); + emitter.instruction("mov x10, #1"); // a subject with no delimiter still holds one segment + emitter.instruction("str x10, [sp, #80]"); // seed the running segment total + emitter.instruction("str xzr, [sp, #88]"); // count from the start of the subject + emitter.label("__rt_explode_count_loop"); + emitter.instruction("bl __rt_explode_find"); // locate the next delimiter occurrence + emitter.instruction("cmp x0, #0"); // did the scan run out of delimiters? + emitter.instruction("b.lt __rt_explode_count_done"); // stop counting once no delimiter remains + emitter.instruction("ldr x10, [sp, #80]"); // reload the running segment total + emitter.instruction("add x10, x10, #1"); // one delimiter introduces one more segment + emitter.instruction("str x10, [sp, #80]"); // publish the updated segment total + emitter.instruction("ldr x11, [sp, #8]"); // reload the delimiter length + emitter.instruction("add x0, x0, x11"); // resume counting after the matched delimiter + emitter.instruction("str x0, [sp, #88]"); // publish the next delimiter-scan start + emitter.instruction("b __rt_explode_count_loop"); // continue counting delimiter occurrences + emitter.label("__rt_explode_count_done"); + emitter.instruction("ldr x10, [sp, #80]"); // reload the final segment total + emitter.instruction("ldr x9, [sp, #56]"); // reload the negative $limit + emitter.instruction("add x9, x10, x9"); // cap = segment total + negative limit + emitter.instruction("cmp x9, #0"); // does the limit drop every segment? + emitter.instruction("b.le __rt_explode_return_array"); // PHP returns an empty array when it does + emitter.instruction("str x9, [sp, #56]"); // publish the element cap + emitter.instruction("str xzr, [sp, #64]"); // negative limits never extend the final element + + // -- emit the retained segments -- + emitter.label("__rt_explode_scan_init"); + emitter.instruction("str xzr, [sp, #40]"); // scan position starts at the beginning of the subject + emitter.instruction("str xzr, [sp, #48]"); // first segment starts at the beginning of the subject + emitter.instruction("str xzr, [sp, #72]"); // no elements have been emitted yet - // -- main loop: scan for delimiter occurrences -- emitter.label("__rt_explode_loop"); - emitter.instruction("ldp x3, x4, [sp, #16]"); // reload string ptr and length - emitter.instruction("ldr x13, [sp, #40]"); // reload current scan position - emitter.instruction("cmp x13, x4"); // check if past end of string - emitter.instruction("b.ge __rt_explode_last"); // if done, push final segment + emitter.instruction("ldr x9, [sp, #72]"); // reload the emitted element count + emitter.instruction("ldr x10, [sp, #56]"); // reload the element cap + emitter.instruction("cmp x9, x10"); // has the limit already been reached? + emitter.instruction("b.ge __rt_explode_return_array"); // stop without a trailing element when it has + emitter.instruction("ldr x11, [sp, #64]"); // reload the extend-last-element rule + emitter.instruction("cbz x11, __rt_explode_next_delim"); // negative limits always emit plain segments + emitter.instruction("add x9, x9, #1"); // would this element be the last one the limit allows? + emitter.instruction("cmp x9, x10"); // compare the prospective count against the cap + emitter.instruction("b.ge __rt_explode_last"); // the last allowed element absorbs the remaining suffix - // -- check if delimiter fits at current position -- - emitter.instruction("ldp x1, x2, [sp]"); // reload delimiter ptr and length - emitter.instruction("sub x14, x4, x13"); // remaining = string_len - scan_pos - emitter.instruction("cmp x2, x14"); // check if delimiter fits in remaining - emitter.instruction("b.gt __rt_explode_last"); // delimiter longer than remaining, done - - // -- compare delimiter at current position -- - emitter.instruction("mov x15, #0"); // delimiter comparison index = 0 - emitter.label("__rt_explode_cmp"); - emitter.instruction("cmp x15, x2"); // check if all delimiter bytes matched - emitter.instruction("b.ge __rt_explode_match"); // full match, delimiter found - emitter.instruction("add x16, x13, x15"); // compute string index = scan_pos + cmp_idx - emitter.instruction("ldrb w17, [x3, x16]"); // load string byte at computed index - emitter.instruction("ldrb w18, [x1, x15]"); // load delimiter byte at cmp index - emitter.instruction("cmp w17, w18"); // compare string and delimiter bytes - emitter.instruction("b.ne __rt_explode_advance"); // mismatch, advance by 1 - emitter.instruction("add x15, x15, #1"); // advance delimiter index - emitter.instruction("b __rt_explode_cmp"); // continue comparing - - // -- no match: advance scan position by 1 -- - emitter.label("__rt_explode_advance"); - emitter.instruction("add x13, x13, #1"); // move scan position forward by 1 - emitter.instruction("str x13, [sp, #40]"); // save updated scan position - emitter.instruction("b __rt_explode_loop"); // continue scanning + emitter.label("__rt_explode_next_delim"); + emitter.instruction("ldr x9, [sp, #40]"); // reload the current scan position + emitter.instruction("str x9, [sp, #88]"); // hand it to the delimiter-scan subroutine + emitter.instruction("bl __rt_explode_find"); // locate the next delimiter occurrence + emitter.instruction("cmp x0, #0"); // did the scan run out of delimiters? + emitter.instruction("b.lt __rt_explode_last"); // the remaining suffix becomes the final element + emitter.instruction("str x0, [sp, #40]"); // remember where the matched delimiter starts - // -- delimiter found: push segment before it to array -- - emitter.label("__rt_explode_match"); + // -- push the segment that precedes the matched delimiter -- emitter.instruction("ldr x0, [sp, #32]"); // load array pointer - emitter.instruction("ldp x3, x4, [sp, #16]"); // reload string ptr and length + emitter.instruction("ldr x3, [sp, #16]"); // reload the subject pointer emitter.instruction("ldr x16, [sp, #48]"); // load segment start position - emitter.instruction("add x1, x3, x16"); // segment ptr = string + segment_start - emitter.instruction("sub x2, x13, x16"); // segment len = scan_pos - segment_start + emitter.instruction("ldr x17, [sp, #40]"); // load the matched delimiter position + emitter.instruction("add x1, x3, x16"); // segment ptr = subject + segment_start + emitter.instruction("sub x2, x17, x16"); // segment len = match_pos - segment_start emitter.instruction("bl __rt_array_push_str"); // push segment string to array emitter.instruction("str x0, [sp, #32]"); // update array pointer after possible realloc + emitter.instruction("ldr x9, [sp, #72]"); // reload the emitted element count + emitter.instruction("add x9, x9, #1"); // one more element has been emitted + emitter.instruction("str x9, [sp, #72]"); // publish the updated emitted count // -- advance past delimiter, update segment start -- - emitter.instruction("ldp x1, x2, [sp]"); // reload delimiter ptr and length - emitter.instruction("ldr x13, [sp, #40]"); // reload scan position - emitter.instruction("add x13, x13, x2"); // skip past delimiter - emitter.instruction("str x13, [sp, #40]"); // save new scan position - emitter.instruction("str x13, [sp, #48]"); // update segment start to after delimiter + emitter.instruction("ldr x11, [sp, #8]"); // reload delimiter length + emitter.instruction("ldr x17, [sp, #40]"); // reload the matched delimiter position + emitter.instruction("add x17, x17, x11"); // skip past delimiter + emitter.instruction("str x17, [sp, #40]"); // save new scan position + emitter.instruction("str x17, [sp, #48]"); // update segment start to after delimiter emitter.instruction("b __rt_explode_loop"); // continue scanning // -- push final segment (from last delimiter to end of string) -- emitter.label("__rt_explode_last"); emitter.instruction("ldr x0, [sp, #32]"); // load array pointer - emitter.instruction("ldp x3, x4, [sp, #16]"); // reload string ptr and length + emitter.instruction("ldp x3, x4, [sp, #16]"); // reload subject ptr and length emitter.instruction("ldr x16, [sp, #48]"); // load segment start position - emitter.instruction("add x1, x3, x16"); // segment ptr = string + segment_start - emitter.instruction("sub x2, x4, x16"); // segment len = string_len - segment_start + emitter.instruction("add x1, x3, x16"); // segment ptr = subject + segment_start + emitter.instruction("sub x2, x4, x16"); // segment len = subject_len - segment_start emitter.instruction("bl __rt_array_push_str"); // push final segment to array emitter.instruction("str x0, [sp, #32]"); // update array pointer after possible realloc // -- return array and restore frame -- + emitter.label("__rt_explode_return_array"); emitter.instruction("ldr x0, [sp, #32]"); // return array pointer in x0 - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // deallocate stack frame emitter.instruction("ret"); // return to caller + + // -- local subroutine: first delimiter at or after [sp+88], or -1 -- + emitter.comment("--- runtime: explode delimiter scan (local subroutine) ---"); + emitter.label("__rt_explode_find"); + emitter.instruction("ldp x1, x2, [sp]"); // reload delimiter ptr and length + emitter.instruction("cbz x2, __rt_explode_find_none"); // a zero-length separator can never match + emitter.instruction("ldp x3, x4, [sp, #16]"); // reload subject ptr and length + emitter.instruction("ldr x9, [sp, #88]"); // load the requested scan start position + emitter.label("__rt_explode_find_loop"); + emitter.instruction("sub x10, x4, x9"); // remaining = subject_len - scan_pos + emitter.instruction("cmp x2, x10"); // check if delimiter still fits in the remainder + emitter.instruction("b.gt __rt_explode_find_none"); // delimiter longer than remaining, no match + emitter.instruction("mov x11, #0"); // delimiter comparison index = 0 + emitter.label("__rt_explode_find_cmp"); + emitter.instruction("cmp x11, x2"); // check if all delimiter bytes matched + emitter.instruction("b.ge __rt_explode_find_hit"); // full match, delimiter found + emitter.instruction("add x12, x9, x11"); // compute subject index = scan_pos + cmp_idx + emitter.instruction("ldrb w14, [x3, x12]"); // load subject byte at computed index + emitter.instruction("ldrb w15, [x1, x11]"); // load delimiter byte at cmp index + emitter.instruction("cmp w14, w15"); // compare subject and delimiter bytes + emitter.instruction("b.ne __rt_explode_find_next"); // mismatch, advance by 1 + emitter.instruction("add x11, x11, #1"); // advance delimiter index + emitter.instruction("b __rt_explode_find_cmp"); // continue comparing + emitter.label("__rt_explode_find_next"); + emitter.instruction("add x9, x9, #1"); // move scan position forward by 1 + emitter.instruction("b __rt_explode_find_loop"); // continue scanning + emitter.label("__rt_explode_find_hit"); + emitter.instruction("mov x0, x9"); // return the matched delimiter position + emitter.instruction("ret"); // return to the explode() scan loop + emitter.label("__rt_explode_find_none"); + emitter.instruction("mov x0, #-1"); // report that no delimiter remains + emitter.instruction("ret"); // return to the explode() scan loop } -/// Emits the x86_64 Linux implementation of `__rt_explode`. +/// Emits the x86_64 implementation of `__rt_explode`. /// -/// Dispatches from `emit_explode` when targeting x86_64 Linux. Uses the AMD64 System V -/// ABI: delimiter pointer/length in rdi/rdx, subject string pointer/length in rsi/rsi, -/// result array pointer returned in rax. Uses rbp-relative frame layout in the red zone -/// to preserve callee-saved registers (delimiter pair at [rbp-8]/[rbp-16], subject pair -/// at [rbp-24]/[rbp-32], array pointer at [rbp-40], scan position at [rbp-48], segment -/// start at [rbp-56]) across helper calls that may clobber caller-saved registers. +/// Dispatches from `emit_explode` when targeting x86_64. Uses the AMD64 System V ABI +/// registers elephc's string lowering materializes: delimiter pointer/length in rax/rdx, +/// subject pointer/length in rdi/rsi, `$limit` in rcx, result array pointer returned in +/// rax. Uses an rbp-relative frame (delimiter pair at `[rbp-8]`/`[rbp-16]`, subject pair at +/// `[rbp-24]`/`[rbp-32]`, array pointer at `[rbp-40]`, scan position at `[rbp-48]`, segment +/// start at `[rbp-56]`, element cap at `[rbp-64]`, extend-last rule at `[rbp-72]`, emitted +/// count at `[rbp-80]`, segment total at `[rbp-88]`, delimiter-scan start at `[rbp-96]`) so +/// every value survives the helper calls that clobber caller-saved registers. fn emit_explode_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: explode ---"); @@ -129,76 +206,135 @@ fn emit_explode_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // preserve the caller frame pointer before the splitter uses stack-backed scan state emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the saved delimiter, subject string, and scan cursors - emitter.instruction("sub rsp, 64"); // reserve aligned local storage for the saved delimiter pair, subject pair, array pointer, and scan indices + emitter.instruction("sub rsp, 112"); // reserve aligned local storage for the saved strings, limit bookkeeping, and scan indices emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the delimiter pointer so every scan iteration can reload it without depending on caller-saved registers emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the delimiter length so the fit check survives helper calls and loop back-edges emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the subject-string pointer so every scan iteration can reload it without depending on caller-saved registers emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the subject-string length so the fit and final-segment checks survive helper calls + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // save the raw PHP $limit before it becomes an element cap emitter.instruction("mov rdi, 16"); // request an initial indexed-array capacity of sixteen string slots for explode() emitter.instruction("mov rsi, 16"); // declare that each explode() element occupies sixteen bytes as a ptr+len string slot emitter.instruction("call __rt_array_new"); // allocate the initial indexed array that will receive each extracted string segment emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the indexed-array pointer because every push helper may reallocate it - emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // initialize the scan position to the start of the subject string - emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // initialize the current segment start position to the start of the subject string + + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the raw $limit before classifying it + emitter.instruction("cmp rcx, 0"); // classify the limit as positive, zero, or negative + emitter.instruction("jg __rt_explode_cap_positive_linux_x86_64"); // a positive limit is already the element cap + emitter.instruction("jl __rt_explode_cap_negative_linux_x86_64"); // a negative limit drops that many trailing segments + emitter.instruction("mov rcx, 1"); // PHP treats $limit === 0 exactly like $limit === 1 + emitter.label("__rt_explode_cap_positive_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // publish the element cap + emitter.instruction("mov QWORD PTR [rbp - 72], 1"); // positive limits let the final element absorb the rest of the subject + emitter.instruction("jmp __rt_explode_scan_init_linux_x86_64"); // start emitting segments + + emitter.label("__rt_explode_cap_negative_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 88], 1"); // a subject with no delimiter still holds one segment + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // count from the start of the subject + emitter.label("__rt_explode_count_loop_linux_x86_64"); + emitter.instruction("call __rt_explode_find_linux_x86_64"); // locate the next delimiter occurrence + emitter.instruction("cmp rax, 0"); // did the scan run out of delimiters? + emitter.instruction("jl __rt_explode_count_done_linux_x86_64"); // stop counting once no delimiter remains + emitter.instruction("mov rcx, QWORD PTR [rbp - 88]"); // reload the running segment total + emitter.instruction("add rcx, 1"); // one delimiter introduces one more segment + emitter.instruction("mov QWORD PTR [rbp - 88], rcx"); // publish the updated segment total + emitter.instruction("add rax, QWORD PTR [rbp - 16]"); // resume counting after the matched delimiter + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // publish the next delimiter-scan start + emitter.instruction("jmp __rt_explode_count_loop_linux_x86_64"); // continue counting delimiter occurrences + emitter.label("__rt_explode_count_done_linux_x86_64"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 88]"); // reload the final segment total + emitter.instruction("add rcx, QWORD PTR [rbp - 64]"); // cap = segment total + negative limit + emitter.instruction("cmp rcx, 0"); // does the limit drop every segment? + emitter.instruction("jle __rt_explode_return_array_linux_x86_64"); // PHP returns an empty array when it does + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // publish the element cap + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // negative limits never extend the final element + + emitter.label("__rt_explode_scan_init_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // scan position starts at the beginning of the subject + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // first segment starts at the beginning of the subject + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // no elements have been emitted yet emitter.label("__rt_explode_loop_linux_x86_64"); - emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the current scan position before checking whether the subject string has been exhausted - emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // stop scanning once the scan position reaches the subject-string length - emitter.instruction("jae __rt_explode_last_linux_x86_64"); // append the trailing segment when the scan position reaches the end of the subject string - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the delimiter length before checking whether it still fits at the current scan position - emitter.instruction("mov r9, QWORD PTR [rbp - 32]"); // reload the subject-string length before computing the remaining scan window - emitter.instruction("sub r9, rcx"); // compute the number of subject bytes remaining at the current scan position - emitter.instruction("cmp r8, r9"); // stop scanning once the delimiter becomes longer than the remaining subject-string suffix - emitter.instruction("ja __rt_explode_last_linux_x86_64"); // append the trailing segment when the delimiter can no longer fit in the remaining subject suffix - - emitter.instruction("xor r10, r10"); // start the delimiter-comparison byte index at zero before checking the current scan position - emitter.label("__rt_explode_cmp_linux_x86_64"); - emitter.instruction("cmp r10, r8"); // stop comparing once every delimiter byte has matched at the current scan position - emitter.instruction("jae __rt_explode_match_linux_x86_64"); // treat the current scan position as a delimiter hit when every delimiter byte matched - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before reading the candidate byte at the current scan position - emitter.instruction("mov rax, rcx"); // seed the subject-byte offset with the current scan position - emitter.instruction("add rax, r10"); // add the delimiter-comparison byte index to form the exact subject-byte offset to test - emitter.instruction("mov dl, BYTE PTR [r11 + rax]"); // load the subject byte that should match the delimiter byte at the same comparison index - emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the delimiter pointer before reading the delimiter byte for the same comparison index - emitter.instruction("mov al, BYTE PTR [r11 + r10]"); // load the delimiter byte that should match the subject byte at the current comparison index - emitter.instruction("cmp dl, al"); // compare the subject and delimiter bytes at the current comparison index - emitter.instruction("jne __rt_explode_advance_linux_x86_64"); // abandon the current scan position when any delimiter byte mismatches the subject - emitter.instruction("add r10, 1"); // advance the delimiter-comparison byte index after one successful byte match - emitter.instruction("jmp __rt_explode_cmp_linux_x86_64"); // continue comparing the remaining delimiter bytes at the current scan position - - emitter.label("__rt_explode_advance_linux_x86_64"); - emitter.instruction("add rcx, 1"); // advance the scan position by one subject byte after a delimiter mismatch - emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // publish the advanced scan position before starting the next scan iteration - emitter.instruction("jmp __rt_explode_loop_linux_x86_64"); // continue scanning the subject string for the next delimiter occurrence - - emitter.label("__rt_explode_match_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the indexed-array pointer before pushing the subject segment that precedes the matched delimiter - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before forming the segment substring pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // reload the emitted element count + emitter.instruction("cmp rcx, QWORD PTR [rbp - 64]"); // has the limit already been reached? + emitter.instruction("jge __rt_explode_return_array_linux_x86_64"); // stop without a trailing element when it has + emitter.instruction("cmp QWORD PTR [rbp - 72], 0"); // reload the extend-last-element rule + emitter.instruction("je __rt_explode_next_delim_linux_x86_64"); // negative limits always emit plain segments + emitter.instruction("add rcx, 1"); // would this element be the last one the limit allows? + emitter.instruction("cmp rcx, QWORD PTR [rbp - 64]"); // compare the prospective count against the cap + emitter.instruction("jge __rt_explode_last_linux_x86_64"); // the last allowed element absorbs the remaining suffix + + emitter.label("__rt_explode_next_delim_linux_x86_64"); + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the current scan position + emitter.instruction("mov QWORD PTR [rbp - 96], rcx"); // hand it to the delimiter-scan subroutine + emitter.instruction("call __rt_explode_find_linux_x86_64"); // locate the next delimiter occurrence + emitter.instruction("cmp rax, 0"); // did the scan run out of delimiters? + emitter.instruction("jl __rt_explode_last_linux_x86_64"); // the remaining suffix becomes the final element + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // remember where the matched delimiter starts + + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the indexed-array pointer into the x86_64 receiver register expected by the string-append helper + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before forming the segment substring pointer emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the current segment start position before computing the substring pointer and length - emitter.instruction("lea rsi, [r11 + r8]"); // compute the segment substring pointer from the subject-string base plus the saved segment start offset - emitter.instruction("mov rdx, rcx"); // seed the segment length with the current scan position where the delimiter match starts - emitter.instruction("sub rdx, r8"); // convert the scan position into the segment length by subtracting the saved segment start offset - emitter.instruction("mov rdi, rax"); // move the indexed-array pointer into the x86_64 receiver register expected by the string-append helper + emitter.instruction("add rsi, r8"); // compute the segment substring pointer from the subject base plus the segment start offset + emitter.instruction("mov rdx, rax"); // seed the segment length with the matched delimiter position + emitter.instruction("sub rdx, r8"); // convert that position into the segment length by subtracting the segment start offset emitter.instruction("call __rt_array_push_str"); // append the subject segment that precedes the matched delimiter to the indexed result array emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the possibly-reallocated indexed-array pointer returned by the string-append helper - emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the scan position because the string-append helper may clobber caller-saved registers - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the delimiter length so the scan position can skip past the full matched delimiter - emitter.instruction("add rcx, r8"); // advance the scan position to the first subject byte after the matched delimiter - emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // publish the advanced scan position after skipping the matched delimiter + emitter.instruction("mov rcx, QWORD PTR [rbp - 80]"); // reload the emitted element count + emitter.instruction("add rcx, 1"); // one more element has been emitted + emitter.instruction("mov QWORD PTR [rbp - 80], rcx"); // publish the updated emitted count + emitter.instruction("mov rcx, QWORD PTR [rbp - 48]"); // reload the matched delimiter position + emitter.instruction("add rcx, QWORD PTR [rbp - 16]"); // advance past the full matched delimiter + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // publish the advanced scan position emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // start the next segment immediately after the matched delimiter - emitter.instruction("jmp __rt_explode_loop_linux_x86_64"); // continue scanning for subsequent delimiter occurrences in the remaining subject suffix + emitter.instruction("jmp __rt_explode_loop_linux_x86_64"); // continue scanning for subsequent delimiter occurrences emitter.label("__rt_explode_last_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the indexed-array pointer before pushing the trailing subject segment - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before forming the trailing substring pointer + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the indexed-array pointer into the x86_64 receiver register expected by the string-append helper + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before forming the trailing substring pointer emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the trailing segment start position saved after the last delimiter match - emitter.instruction("lea rsi, [r11 + r8]"); // compute the trailing segment pointer from the subject-string base plus the saved segment start offset + emitter.instruction("add rsi, r8"); // compute the trailing segment pointer from the subject base plus the segment start offset emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // seed the trailing segment length with the full subject-string length - emitter.instruction("sub rdx, r8"); // compute the trailing segment length from the full subject length minus the saved segment start offset - emitter.instruction("mov rdi, rax"); // move the indexed-array pointer into the x86_64 receiver register expected by the string-append helper - emitter.instruction("call __rt_array_push_str"); // append the trailing subject segment after the final delimiter occurrence to the indexed result array - emitter.instruction("add rsp, 64"); // release the splitter locals after the final segment has been appended to the result array + emitter.instruction("sub rdx, r8"); // compute the trailing segment length from the full subject length minus the segment start offset + emitter.instruction("call __rt_array_push_str"); // append the trailing subject segment to the indexed result array + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the possibly-reallocated indexed-array pointer returned by the string-append helper + + emitter.label("__rt_explode_return_array_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the indexed explode() result array pointer + emitter.instruction("add rsp, 112"); // release the splitter locals after the result array is final emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the indexed explode() result array emitter.instruction("ret"); // return the indexed explode() result array pointer in the standard x86_64 integer result register + + emitter.comment("--- runtime: explode delimiter scan (local subroutine) ---"); + emitter.label("__rt_explode_find_linux_x86_64"); + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the delimiter length before scanning + emitter.instruction("test r8, r8"); // is the separator zero-length? + emitter.instruction("jz __rt_explode_find_none_linux_x86_64"); // a zero-length separator can never match + emitter.instruction("mov rax, QWORD PTR [rbp - 96]"); // load the requested scan start position + emitter.label("__rt_explode_find_loop_linux_x86_64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 32]"); // reload the subject-string length before computing the remaining scan window + emitter.instruction("sub r9, rax"); // compute the number of subject bytes remaining at the current scan position + emitter.instruction("cmp r8, r9"); // does the delimiter still fit in the remaining suffix? + emitter.instruction("jg __rt_explode_find_none_linux_x86_64"); // report no match once the delimiter no longer fits + emitter.instruction("xor r10, r10"); // start the delimiter-comparison byte index at zero + emitter.label("__rt_explode_find_cmp_linux_x86_64"); + emitter.instruction("cmp r10, r8"); // stop comparing once every delimiter byte has matched + emitter.instruction("jae __rt_explode_find_hit_linux_x86_64"); // treat the current scan position as a delimiter hit + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the subject-string pointer before reading the candidate byte + emitter.instruction("add r11, rax"); // advance to the current scan position inside the subject + emitter.instruction("movzx ecx, BYTE PTR [r11 + r10]"); // load the subject byte that should match the delimiter byte + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the delimiter pointer before reading the delimiter byte + emitter.instruction("movzx r9d, BYTE PTR [r11 + r10]"); // load the delimiter byte for the current comparison index + emitter.instruction("cmp ecx, r9d"); // compare the subject and delimiter bytes + emitter.instruction("jne __rt_explode_find_next_linux_x86_64"); // abandon the current scan position on any mismatch + emitter.instruction("add r10, 1"); // advance the delimiter-comparison byte index + emitter.instruction("jmp __rt_explode_find_cmp_linux_x86_64"); // continue comparing the remaining delimiter bytes + emitter.label("__rt_explode_find_next_linux_x86_64"); + emitter.instruction("add rax, 1"); // advance the scan position by one subject byte + emitter.instruction("jmp __rt_explode_find_loop_linux_x86_64"); // continue scanning the subject for the next delimiter occurrence + emitter.label("__rt_explode_find_hit_linux_x86_64"); + emitter.instruction("ret"); // return the matched delimiter position already held in rax + emitter.label("__rt_explode_find_none_linux_x86_64"); + emitter.instruction("mov rax, -1"); // report that no delimiter remains + emitter.instruction("ret"); // return to the explode() scan loop } diff --git a/src/codegen_support/runtime/strings/ftoa.rs b/src/codegen_support/runtime/strings/ftoa.rs index afbb65772c..7af3e47615 100644 --- a/src/codegen_support/runtime/strings/ftoa.rs +++ b/src/codegen_support/runtime/strings/ftoa.rs @@ -1,16 +1,33 @@ //! Purpose: //! Emits the `__rt_ftoa` runtime helper assembly for float-to-string conversion. -//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! Reproduces PHP's default `precision = 14` layout (`echo`, `(string)`, `print_r`, +//! string interpolation) and, through `__rt_ftoa_repr`, PHP's +//! `serialize_precision = -1` layout used by `var_dump`. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - PHP formats a float for `echo` with `zend_gcvt(value, 14, '.', 'E')`. C's `%.14G` +//! already picks the *same* notation (exponential when the decimal exponent is `>= 14` +//! or `<= -5`) and the same 14-significant-digit rounding, but it differs in two +//! byte-level details that `__rt_ftoa` fixes up while copying the snprintf scratch into +//! `_concat_buf`: +//! 1. `zend_gcvt` always writes a fraction in exponential form, so a one-digit mantissa +//! becomes `1.0E+300`, never C's `1E+300`. +//! 2. `zend_gcvt` writes the exponent with no leading zeros, so `1.0E-7`, never C's +//! `1E-07`. +//! - `NAN` is normalized to the unsigned spelling PHP prints; glibc renders a negative +//! quiet NaN as `-NAN`, which PHP never does. +//! - `__rt_ftoa_repr` answers `var_dump`'s `%.*H` at `serialize_precision = -1`: the +//! shortest decimal string that round-trips. The finite case is exactly +//! `__rt_json_ftoa` with an uppercase `E` marker, so this helper only owns the +//! `INF`/`-INF`/`NAN` spellings that `__rt_json_ftoa`'s caller normally handles. -use crate::codegen_support::{emit::Emitter, platform::Arch}; +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; -/// Converts a double-precision float to a PHP-compatible byte string. +/// Converts a double-precision float to a PHP-compatible byte string at `precision = 14`. /// /// # Input /// - ARM64: `d0` holds the float value @@ -21,9 +38,11 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// - x86_64: `rax` = pointer to string, `rdx` = length /// /// # Behavior -/// Formats the float using `snprintf` with `"%.14G"` format into the global -/// `_concat_buf` buffer at the current `_concat_off` cursor, then advances -/// `_concat_off` by the number of characters written. +/// Formats the float with `snprintf("%.14G", …)` into a stack scratch buffer, then copies +/// the bytes into the global `_concat_buf` at the current `_concat_off` cursor while +/// applying PHP's `zend_gcvt` fixups (mandatory `.0` mantissa fraction in exponential +/// form, unpadded exponent, unsigned `NAN`), and advances `_concat_off` by the number of +/// bytes actually emitted. /// /// # ABI Notes /// - Apple ARM64: variadic floats are passed on the stack, not in SIMD registers @@ -35,46 +54,103 @@ pub fn emit_ftoa(emitter: &mut Emitter) { } emitter.blank(); - emitter.comment("--- runtime: ftoa ---"); + emitter.comment("--- runtime: ftoa (precision=14, PHP zend_gcvt layout) ---"); emitter.label_global("__rt_ftoa"); - // -- set up stack frame (64 bytes) -- - emitter.instruction("sub sp, sp, #64"); // allocate 64 bytes on the stack - emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #48"); // establish new frame pointer + // -- set up stack frame (80 bytes: variadic slot, 48-byte scratch, saved FP/LR) -- + emitter.instruction("sub sp, sp, #80"); // allocate the variadic slot, snprintf scratch, and saved-register area + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #64"); // establish new frame pointer - // -- get current concat_buf position -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current write offset - emitter.instruction("str x10, [sp, #32]"); // save original offset on stack - emitter.instruction("str x9, [sp, #40]"); // save offset variable address on stack - - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x0, x11, x10"); // compute output buffer: concat_buf + offset - emitter.instruction("str x0, [sp, #24]"); // save output buffer start on stack - - // -- call snprintf(buf, 32, "%.14G", double) -- - emitter.instruction("mov x1, #32"); // buffer size limit = 32 bytes - crate::codegen_support::abi::emit_symbol_address(emitter, "x2", "_fmt_g"); // load page address of format string "%.14G" + // -- call snprintf(scratch, 48, "%.14G", double) -- + emitter.instruction("add x0, sp, #8"); // snprintf destination = stack scratch buffer + emitter.instruction("mov x1, #48"); // scratch buffer size limit + abi::emit_symbol_address(emitter, "x2", "_fmt_g"); // -- Apple ARM64 variadic ABI: float arg goes on stack, not in SIMD reg -- emitter.instruction("str d0, [sp]"); // push double onto stack for variadic call - emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 + emitter.bl_c("snprintf"); // format the double at 14 significant digits + + // -- destination cursor inside _concat_buf -- + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // load the current concat write offset + abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("add x13, x11, x10"); // result start = concat_buf + offset + emitter.instruction("mov x12, x13"); // x12 = write cursor, x13 = result start + emitter.instruction("add x14, sp, #8"); // x14 = read cursor into the snprintf scratch + emitter.instruction("mov w15, #0"); // w15 = "mantissa already has a '.'" flag + + // -- PHP never prints a signed NAN: collapse "NAN"/"-NAN" to "NAN" -- + emitter.instruction("ldrb w16, [x14]"); // first formatted byte + emitter.instruction("cmp w16, #45"); // is it an ASCII '-' sign? + emitter.instruction("b.ne __rt_ftoa_nan_check"); // unsigned text: inspect the first byte directly + emitter.instruction("ldrb w16, [x14, #1]"); // signed text: inspect the byte after the sign + emitter.label("__rt_ftoa_nan_check"); + emitter.instruction("cmp w16, #78"); // ASCII 'N' can only start the NAN spelling + emitter.instruction("b.ne __rt_ftoa_copy"); // ordinary numeric text: run the copy/fixup loop + emitter.instruction("mov w17, #78"); // ASCII 'N' + emitter.instruction("strb w17, [x12], #1"); // emit 'N' + emitter.instruction("mov w17, #65"); // ASCII 'A' + emitter.instruction("strb w17, [x12], #1"); // emit 'A' + emitter.instruction("mov w17, #78"); // ASCII 'N' + emitter.instruction("strb w17, [x12], #1"); // emit 'N' + emitter.instruction("b __rt_ftoa_finish"); // NAN needs no further fixups + + // -- copy the mantissa, remembering whether it already contains a '.' -- + emitter.label("__rt_ftoa_copy"); + emitter.instruction("ldrb w16, [x14]"); // load the next scratch byte + emitter.instruction("cbz w16, __rt_ftoa_finish"); // NUL terminator: decimal form needs no fixup + emitter.instruction("cmp w16, #69"); // ASCII 'E' starts the exponent part + emitter.instruction("b.eq __rt_ftoa_exp"); // switch to the exponential fixup path + emitter.instruction("cmp w16, #46"); // ASCII '.' marks an existing mantissa fraction + emitter.instruction("b.ne __rt_ftoa_copy_store"); // no fraction marker: just copy the byte + emitter.instruction("mov w15, #1"); // record that the mantissa already has a fraction + emitter.label("__rt_ftoa_copy_store"); + emitter.instruction("strb w16, [x12], #1"); // emit the mantissa byte + emitter.instruction("add x14, x14, #1"); // advance the scratch read cursor + emitter.instruction("b __rt_ftoa_copy"); // continue copying the mantissa + + // -- exponential form: zend_gcvt always writes a fraction, C's %G does not -- + emitter.label("__rt_ftoa_exp"); + emitter.instruction("cbnz w15, __rt_ftoa_exp_marker"); // mantissa already has a fraction + emitter.instruction("mov w17, #46"); // ASCII '.' + emitter.instruction("strb w17, [x12], #1"); // emit the mandatory decimal point + emitter.instruction("mov w17, #48"); // ASCII '0' + emitter.instruction("strb w17, [x12], #1"); // emit the mandatory "0" fraction digit + emitter.label("__rt_ftoa_exp_marker"); + emitter.instruction("strb w16, [x12], #1"); // emit the 'E' exponent marker + emitter.instruction("add x14, x14, #1"); // advance past 'E' in the scratch + emitter.instruction("ldrb w16, [x14]"); // load the exponent sign byte + emitter.instruction("strb w16, [x12], #1"); // emit the exponent sign + emitter.instruction("add x14, x14, #1"); // advance past the exponent sign - // -- x0 = number of chars written -- - emitter.instruction("mov x2, x0"); // save string length as return value + // -- zend_gcvt writes the exponent unpadded, C's %G pads it to two digits -- + emitter.label("__rt_ftoa_exp_strip"); + emitter.instruction("ldrb w16, [x14]"); // load the next exponent digit + emitter.instruction("cmp w16, #48"); // is it a padding ASCII '0'? + emitter.instruction("b.ne __rt_ftoa_exp_digits"); // significant digit: stop stripping + emitter.instruction("ldrb w17, [x14, #1]"); // peek at the following byte + emitter.instruction("cbz w17, __rt_ftoa_exp_digits"); // keep a lone '0' as the exponent value + emitter.instruction("add x14, x14, #1"); // drop one leading zero + emitter.instruction("b __rt_ftoa_exp_strip"); // keep stripping leading zeros - // -- update concat_off by chars written -- - emitter.instruction("ldr x9, [sp, #40]"); // reload offset variable address - emitter.instruction("ldr x10, [sp, #32]"); // reload original offset - emitter.instruction("add x10, x10, x2"); // new offset = original + chars written - emitter.instruction("str x10, [x9]"); // store updated offset + emitter.label("__rt_ftoa_exp_digits"); + emitter.instruction("ldrb w16, [x14]"); // load the next exponent digit + emitter.instruction("cbz w16, __rt_ftoa_finish"); // NUL terminator ends the exponent + emitter.instruction("strb w16, [x12], #1"); // emit the exponent digit + emitter.instruction("add x14, x14, #1"); // advance the scratch read cursor + emitter.instruction("b __rt_ftoa_exp_digits"); // copy the remaining exponent digits - // -- set return pointer -- - emitter.instruction("ldr x1, [sp, #24]"); // return pointer to start of formatted string + // -- publish the result slice and advance the concat cursor -- + emitter.label("__rt_ftoa_finish"); + emitter.instruction("sub x2, x12, x13"); // result length = cursor - start + emitter.instruction("mov x1, x13"); // result pointer = start of the emitted text + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // reload the original concat offset + emitter.instruction("add x10, x10, x2"); // advance it past the emitted bytes + emitter.instruction("str x10, [x9]"); // publish the updated concat offset - // -- restore frame and return -- - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #64"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // deallocate stack frame emitter.instruction("ret"); // return to caller } @@ -87,44 +163,231 @@ pub fn emit_ftoa(emitter: &mut Emitter) { /// - `rax` = pointer to formatted string, `rdx` = length /// /// # Behavior -/// Same as `emit_ftoa` but for the Linux x86_64 target. Uses `rbp`-based -/// frame with 32 bytes of scratch space for concat cursor and output pointer. +/// Same as `emit_ftoa` but for the Linux x86_64 target: `snprintf("%.14G", …)` into a +/// 48-byte stack scratch buffer, then the same `zend_gcvt` fixup copy into `_concat_buf`. /// Sets `eax = 1` to signal one SIMD register argument to `snprintf`. fn emit_ftoa_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); - emitter.comment("--- runtime: ftoa ---"); + emitter.comment("--- runtime: ftoa (precision=14, PHP zend_gcvt layout) ---"); emitter.label_global("__rt_ftoa"); emitter.instruction("push rbp"); // save the caller frame pointer before using stack locals emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the formatting helper - emitter.instruction("sub rsp, 32"); // reserve aligned scratch space for concat offsets and the output pointer + emitter.instruction("sub rsp, 64"); // reserve aligned scratch space for the snprintf result - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat cursor so formatted bytes append after prior output - emitter.instruction("mov QWORD PTR [rbp - 8], r9"); // save the original concat cursor for the final offset update - emitter.instruction("mov QWORD PTR [rbp - 16], r8"); // save the concat cursor symbol address for the final store + emitter.instruction("lea rdi, [rbp - 56]"); // snprintf destination = stack scratch buffer + emitter.instruction("mov esi, 48"); // scratch buffer size limit + abi::emit_symbol_address(emitter, "rdx", "_fmt_g"); + emitter.instruction("mov eax, 1"); // SysV variadic ABI: one SIMD register is live for the double argument + emitter.instruction("call snprintf"); // format the double at 14 significant digits - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea rdi, [r10 + r9]"); // compute the destination buffer inside the concat scratch area - emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // preserve the destination pointer for the return value + abi::emit_load_symbol_to_reg(emitter, "r9", "_concat_off", 0); // current concat write offset + abi::emit_symbol_address(emitter, "r8", "_concat_buf"); + emitter.instruction("lea r10, [r8 + r9]"); // result start = concat_buf + offset + emitter.instruction("mov r11, r10"); // r11 = write cursor, r10 = result start + emitter.instruction("lea rsi, [rbp - 56]"); // rsi = read cursor into the snprintf scratch + emitter.instruction("xor ecx, ecx"); // ecx = "mantissa already has a '.'" flag - emitter.instruction("mov esi, 32"); // cap float formatting to the same 32-byte scratch window used on AArch64 - crate::codegen_support::abi::emit_symbol_address(emitter, "rdx", "_fmt_g"); - emitter.instruction("mov eax, 1"); // SysV variadic ABI: one SIMD register is live for the double argument - emitter.instruction("call snprintf"); // format xmm0 using "%.14G" into the concat scratch buffer + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // first formatted byte + emitter.instruction("cmp al, 45"); // is it an ASCII '-' sign? + emitter.instruction("jne __rt_ftoa_nan_check_x"); // unsigned text: inspect the first byte directly + emitter.instruction("movzx eax, BYTE PTR [rsi + 1]"); // signed text: inspect the byte after the sign + emitter.label("__rt_ftoa_nan_check_x"); + emitter.instruction("cmp al, 78"); // ASCII 'N' can only start the NAN spelling + emitter.instruction("jne __rt_ftoa_copy_x"); // ordinary numeric text: run the copy/fixup loop + emitter.instruction("mov BYTE PTR [r11], 78"); // emit 'N' + emitter.instruction("mov BYTE PTR [r11 + 1], 65"); // emit 'A' + emitter.instruction("mov BYTE PTR [r11 + 2], 78"); // emit 'N' + emitter.instruction("add r11, 3"); // advance the write cursor past "NAN" + emitter.instruction("jmp __rt_ftoa_finish_x"); // NAN needs no further fixups + + emitter.label("__rt_ftoa_copy_x"); + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the next scratch byte + emitter.instruction("test al, al"); // check for the NUL terminator + emitter.instruction("jz __rt_ftoa_finish_x"); // decimal form needs no fixup + emitter.instruction("cmp al, 69"); // ASCII 'E' starts the exponent part + emitter.instruction("je __rt_ftoa_exp_x"); // switch to the exponential fixup path + emitter.instruction("cmp al, 46"); // ASCII '.' marks an existing mantissa fraction + emitter.instruction("jne __rt_ftoa_copy_store_x"); // no fraction marker: just copy the byte + emitter.instruction("mov ecx, 1"); // record that the mantissa already has a fraction + emitter.label("__rt_ftoa_copy_store_x"); + emitter.instruction("mov BYTE PTR [r11], al"); // emit the mantissa byte + emitter.instruction("inc r11"); // advance the write cursor + emitter.instruction("inc rsi"); // advance the scratch read cursor + emitter.instruction("jmp __rt_ftoa_copy_x"); // continue copying the mantissa + + emitter.label("__rt_ftoa_exp_x"); + emitter.instruction("test ecx, ecx"); // does the mantissa already have a fraction? + emitter.instruction("jnz __rt_ftoa_exp_marker_x"); // yes: keep it as formatted + emitter.instruction("mov BYTE PTR [r11], 46"); // emit the mandatory decimal point + emitter.instruction("mov BYTE PTR [r11 + 1], 48"); // emit the mandatory "0" fraction digit + emitter.instruction("add r11, 2"); // advance the write cursor past ".0" + emitter.label("__rt_ftoa_exp_marker_x"); + emitter.instruction("mov BYTE PTR [r11], 69"); // emit the 'E' exponent marker + emitter.instruction("inc r11"); // advance the write cursor + emitter.instruction("inc rsi"); // advance past 'E' in the scratch + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the exponent sign byte + emitter.instruction("mov BYTE PTR [r11], al"); // emit the exponent sign + emitter.instruction("inc r11"); // advance the write cursor + emitter.instruction("inc rsi"); // advance past the exponent sign + + emitter.label("__rt_ftoa_exp_strip_x"); + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the next exponent digit + emitter.instruction("cmp al, 48"); // is it a padding ASCII '0'? + emitter.instruction("jne __rt_ftoa_exp_digits_x"); // significant digit: stop stripping + emitter.instruction("movzx edx, BYTE PTR [rsi + 1]"); // peek at the following byte + emitter.instruction("test dl, dl"); // is the zero the last exponent digit? + emitter.instruction("jz __rt_ftoa_exp_digits_x"); // keep a lone '0' as the exponent value + emitter.instruction("inc rsi"); // drop one leading zero + emitter.instruction("jmp __rt_ftoa_exp_strip_x"); // keep stripping leading zeros - emitter.instruction("mov rdx, rax"); // return the formatted byte count in the string-length result register - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the concat cursor symbol address - emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the original concat cursor - emitter.instruction("add r9, rdx"); // advance the concat cursor by the number of formatted bytes - emitter.instruction("mov QWORD PTR [r8], r9"); // publish the updated concat cursor for subsequent string writes - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the pointer to the formatted float text + emitter.label("__rt_ftoa_exp_digits_x"); + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the next exponent digit + emitter.instruction("test al, al"); // check for the NUL terminator + emitter.instruction("jz __rt_ftoa_finish_x"); // the exponent is complete + emitter.instruction("mov BYTE PTR [r11], al"); // emit the exponent digit + emitter.instruction("inc r11"); // advance the write cursor + emitter.instruction("inc rsi"); // advance the scratch read cursor + emitter.instruction("jmp __rt_ftoa_exp_digits_x"); // copy the remaining exponent digits - emitter.instruction("add rsp, 32"); // release the local scratch area before returning + emitter.label("__rt_ftoa_finish_x"); + emitter.instruction("mov rax, r10"); // result pointer = start of the emitted text + emitter.instruction("mov rdx, r11"); // write cursor, one past the last byte + emitter.instruction("sub rdx, rax"); // result length = cursor - start + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the original concat offset + emitter.instruction("add r8, rdx"); // advance it past the emitted bytes + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated concat offset + + emitter.instruction("add rsp, 64"); // release the local scratch area before returning emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return pointer+length in rax/rdx } +/// Emits `__rt_ftoa_repr`, PHP's `serialize_precision = -1` float rendering. +/// +/// This is the layout `var_dump()` prints (`%.*H` with `serialize_precision = -1`): the +/// shortest decimal string that round-trips back to the same `double`, with an uppercase +/// `E` marker, a mandatory `d.d` mantissa in exponential form, an unpadded exponent, and +/// NO trailing `.0` for integer-valued floats (`float(100)`, not `float(100.0)`). +/// +/// Finite values are handed to `__rt_json_ftoa` — the tested shortest-round-trip +/// formatter shared with `json_encode`/`serialize` — with `'E'` as the exponent marker. +/// This helper only owns the three non-finite spellings, because `__rt_json_ftoa` relies +/// on its caller to filter them out. +/// +/// Input: AArch64 `d0` / x86_64 `xmm0` = the double to render. +/// Output: AArch64 `x1`/`x2`, x86_64 `rax`/`rdx` = pointer/length inside `_concat_buf`, +/// with `_concat_off` advanced past the emitted bytes — the same ABI as `__rt_ftoa`. +pub fn emit_ftoa_repr(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_ftoa_repr_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: ftoa_repr (serialize_precision=-1, var_dump layout) ---"); + emitter.label_global("__rt_ftoa_repr"); + + // -- classify the double through its raw bits: finite, infinite, or NaN -- + emitter.instruction("fmov x9, d0"); // raw IEEE-754 bit pattern of the value + emitter.instruction("and x10, x9, #0x7fffffffffffffff"); // drop the sign bit to test the magnitude + emitter.instruction("movz x11, #0x7ff0, lsl #48"); // the exact bit pattern of positive infinity + emitter.instruction("cmp x10, x11"); // compare the magnitude against infinity + emitter.instruction("b.hi __rt_ftoa_repr_nan"); // above infinity means NaN + emitter.instruction("b.eq __rt_ftoa_repr_inf"); // exactly infinity + + // -- finite: PHP's shortest round-trip formatter with an uppercase exponent marker -- + emitter.instruction("mov w0, #69"); // ASCII 'E': var_dump uses the uppercase marker + emitter.instruction("b __rt_json_ftoa"); // tail-call the shared shortest-round-trip formatter + + emitter.label("__rt_ftoa_repr_nan"); + emitter.instruction("mov x9, #0"); // no sign byte: PHP always prints an unsigned NAN + emitter.instruction("mov w12, #78"); // ASCII 'N' as the first literal byte + emitter.instruction("mov w13, #65"); // ASCII 'A' as the second literal byte + emitter.instruction("mov w14, #78"); // ASCII 'N' as the third literal byte + emitter.instruction("b __rt_ftoa_repr_emit"); // emit the three-byte literal + + emitter.label("__rt_ftoa_repr_inf"); + emitter.instruction("mov w12, #73"); // ASCII 'I' as the first literal byte + emitter.instruction("mov w13, #78"); // ASCII 'N' as the second literal byte + emitter.instruction("mov w14, #70"); // ASCII 'F' as the third literal byte + + // -- write "[-]XXX" straight into the concat buffer and publish the cursor -- + emitter.label("__rt_ftoa_repr_emit"); + abi::emit_symbol_address(emitter, "x15", "_concat_off"); + emitter.instruction("ldr x16, [x15]"); // current concat write offset + abi::emit_symbol_address(emitter, "x17", "_concat_buf"); + emitter.instruction("add x1, x17, x16"); // result start = concat_buf + offset + emitter.instruction("mov x11, x1"); // x11 = write cursor, x1 = result start + emitter.instruction("tbz x9, #63, __rt_ftoa_repr_body"); // skip the sign byte for non-negative values + emitter.instruction("mov w10, #45"); // ASCII '-' + emitter.instruction("strb w10, [x11], #1"); // emit the sign byte for -INF + emitter.label("__rt_ftoa_repr_body"); + emitter.instruction("strb w12, [x11], #1"); // emit the first literal byte + emitter.instruction("strb w13, [x11], #1"); // emit the second literal byte + emitter.instruction("strb w14, [x11], #1"); // emit the third literal byte + emitter.instruction("sub x2, x11, x1"); // result length = cursor - start + emitter.instruction("add x16, x16, x2"); // advance the concat cursor past the literal + emitter.instruction("str x16, [x15]"); // publish the updated concat offset + emitter.instruction("ret"); // return pointer (x1) and length (x2) +} + +/// Emits the Linux x86_64 variant of `__rt_ftoa_repr`. +/// +/// Mirrors the AArch64 helper: classify the double from its raw bits, tail-call +/// `__rt_json_ftoa` with `'E'` for finite values, and emit `INF` / `-INF` / `NAN` directly +/// into `_concat_buf` otherwise. +/// +/// Input: `xmm0` = the double to render. Output: `rax`/`rdx` = pointer/length. +fn emit_ftoa_repr_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: ftoa_repr (serialize_precision=-1, var_dump layout) ---"); + emitter.label_global("__rt_ftoa_repr"); + + emitter.instruction("movq r9, xmm0"); // raw IEEE-754 bit pattern of the value + emitter.instruction("movabs r10, 0x7fffffffffffffff"); // mask that drops the sign bit + emitter.instruction("and r10, r9"); // magnitude bits of the value + emitter.instruction("movabs r11, 0x7ff0000000000000"); // the exact bit pattern of positive infinity + emitter.instruction("cmp r10, r11"); // compare the magnitude against infinity + emitter.instruction("ja __rt_ftoa_repr_nan_x"); // above infinity means NaN + emitter.instruction("je __rt_ftoa_repr_inf_x"); // exactly infinity + + emitter.instruction("mov edi, 69"); // ASCII 'E': var_dump uses the uppercase marker + emitter.instruction("jmp __rt_json_ftoa"); // tail-call the shared shortest-round-trip formatter + + emitter.label("__rt_ftoa_repr_nan_x"); + emitter.instruction("xor r9d, r9d"); // no sign byte: PHP always prints an unsigned NAN + emitter.instruction("mov ecx, 78"); // ASCII 'N' as the first literal byte + emitter.instruction("mov esi, 65"); // ASCII 'A' as the second literal byte + emitter.instruction("mov edi, 78"); // ASCII 'N' as the third literal byte + emitter.instruction("jmp __rt_ftoa_repr_emit_x"); // emit the three-byte literal + + emitter.label("__rt_ftoa_repr_inf_x"); + emitter.instruction("mov ecx, 73"); // ASCII 'I' as the first literal byte + emitter.instruction("mov esi, 78"); // ASCII 'N' as the second literal byte + emitter.instruction("mov edi, 70"); // ASCII 'F' as the third literal byte + + emitter.label("__rt_ftoa_repr_emit_x"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // current concat write offset + abi::emit_symbol_address(emitter, "r11", "_concat_buf"); + emitter.instruction("lea rax, [r11 + r10]"); // result start = concat_buf + offset + emitter.instruction("mov r8, rax"); // r8 = write cursor, rax = result start + emitter.instruction("test r9, r9"); // is the sign bit set? + emitter.instruction("jns __rt_ftoa_repr_body_x"); // skip the sign byte for non-negative values + emitter.instruction("mov BYTE PTR [r8], 45"); // emit the ASCII '-' for -INF + emitter.instruction("inc r8"); // advance the write cursor + emitter.label("__rt_ftoa_repr_body_x"); + emitter.instruction("mov BYTE PTR [r8], cl"); // emit the first literal byte + emitter.instruction("mov BYTE PTR [r8 + 1], sil"); // emit the second literal byte + emitter.instruction("mov BYTE PTR [r8 + 2], dil"); // emit the third literal byte + emitter.instruction("add r8, 3"); // advance the write cursor past the literal + emitter.instruction("mov rdx, r8"); // write cursor, one past the last byte + emitter.instruction("sub rdx, rax"); // result length = cursor - start + emitter.instruction("add r10, rdx"); // advance the concat cursor past the literal + abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // publish the updated concat offset + emitter.instruction("ret"); // return pointer (rax) and length (rdx) +} + #[cfg(test)] mod tests { use crate::codegen_support::platform::{Arch, Platform, Target}; @@ -132,7 +395,8 @@ mod tests { use super::*; /// Verifies that `emit_ftoa` on Linux x86_64 uses the SysV variadic calling convention - /// by checking that `eax` is set to 1 (one SIMD register argument) before calling `snprintf`. + /// by checking that `eax` is set to 1 (one SIMD register argument) before calling + /// `snprintf`, and that the fixup copy returns pointer/length in `rax`/`rdx`. #[test] fn test_emit_ftoa_linux_x86_64_uses_sysv_variadic_call() { let mut emitter = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); @@ -142,6 +406,38 @@ mod tests { assert!(asm.contains("__rt_ftoa:\n")); assert!(asm.contains("mov eax, 1\n")); assert!(asm.contains("call snprintf\n")); - assert!(asm.contains("mov rdx, rax\n")); + assert!(asm.contains("sub rdx, rax\n")); + } + + /// Verifies that both targets emit the `zend_gcvt` fixup path: the mandatory `.0` + /// mantissa fraction (ASCII 46/48) and the exponent leading-zero strip loop. + #[test] + fn test_emit_ftoa_applies_php_gcvt_fixups() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_ftoa(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_ftoa_exp"), "missing exponential fixup for {:?}", arch); + assert!( + asm.contains("__rt_ftoa_exp_strip") || asm.contains("__rt_ftoa_exp_strip_x"), + "missing exponent zero-strip for {:?}", + arch + ); + } + } + + /// Verifies that `__rt_ftoa_repr` delegates finite values to the shared + /// shortest-round-trip formatter with the uppercase `E` marker (ASCII 69) and owns the + /// non-finite spellings itself. + #[test] + fn test_emit_ftoa_repr_delegates_to_json_ftoa() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_ftoa_repr(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_ftoa_repr:\n"), "missing entry point for {:?}", arch); + assert!(asm.contains("__rt_json_ftoa"), "missing delegation for {:?}", arch); + assert!(asm.contains("69"), "missing uppercase 'E' marker for {:?}", arch); + } } } diff --git a/src/codegen_support/runtime/strings/hex2bin.rs b/src/codegen_support/runtime/strings/hex2bin.rs index 3352affc93..165b986ae5 100644 --- a/src/codegen_support/runtime/strings/hex2bin.rs +++ b/src/codegen_support/runtime/strings/hex2bin.rs @@ -9,7 +9,6 @@ //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. use crate::codegen_support::{emit::Emitter, platform::Arch}; -use crate::codegen_support::abi; /// Emits the `__rt_hex2bin` runtime helper for converting a hex string to binary. /// Dispatches to the target-specific implementation (x86_64 Linux calls @@ -26,11 +25,16 @@ pub fn emit_hex2bin(emitter: &mut Emitter) { emitter.comment("--- runtime: hex2bin ---"); emitter.label_global("__rt_hex2bin"); - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the decoded result before writing anything (1 byte out per 2 in, so len is an upper bound) -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed hexadecimal string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the hex2bin helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the hexadecimal pointer and length across the reservation call + emitter.instruction("mov x0, x2"); // the decoded payload never exceeds the hexadecimal character count + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed hexadecimal pointer and length emitter.instruction("mov x11, x2"); // remaining hex chars emitter.label("__rt_hex2bin_loop"); @@ -75,9 +79,9 @@ pub fn emit_hex2bin(emitter: &mut Emitter) { emitter.label("__rt_hex2bin_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance - emitter.instruction("str x8, [x6]"); // store + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the hex2bin helper frame emitter.instruction("ret"); // return } @@ -89,12 +93,17 @@ fn emit_hex2bin_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: hex2bin ---"); emitter.label_global("__rt_hex2bin"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer offset before appending the decoded bytes - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // load the base address of the shared concat buffer - emitter.instruction("add r9, r8"); // compute the destination pointer at the current concat-buffer tail + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed hexadecimal string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the hexadecimal pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the hexadecimal source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the hexadecimal character count across the reservation call + emitter.instruction("mov rax, rdx"); // the decoded payload never exceeds the hexadecimal character count + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov r9, rax"); // compute the destination pointer at the reserved result start emitter.instruction("mov r10, r9"); // preserve the decoded string start pointer for the return value - emitter.instruction("mov rcx, rdx"); // copy the hexadecimal character count into a decrementing loop counter - emitter.instruction("mov rsi, rax"); // copy the hexadecimal source pointer into a cursor register for byte-by-byte reads + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // copy the hexadecimal character count into a decrementing loop counter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // copy the hexadecimal source pointer into a cursor register for byte-by-byte reads emitter.label("__rt_hex2bin_loop_linux_x86_64"); emitter.instruction("cmp rcx, 2"); // check whether at least one complete hexadecimal pair remains @@ -139,11 +148,10 @@ fn emit_hex2bin_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_hex2bin_done_linux_x86_64"); emitter.instruction("mov rax, r10"); // return the decoded string start pointer in the standard x86_64 string result register - emitter.instruction("mov rdx, r9"); // copy the concat-buffer tail into the length scratch register + emitter.instruction("mov rdx, r9"); // copy the destination cursor into the length scratch register emitter.instruction("sub rdx, r10"); // compute the decoded string length from the written byte count - emitter.instruction("mov r8, r9"); // copy the absolute concat-buffer tail before normalizing it back to a shared offset - abi::emit_symbol_address(emitter, "r11", "_concat_buf"); // load the concat-buffer base so the shared offset can stay relative - emitter.instruction("sub r8, r11"); // convert the absolute concat-buffer tail back into the shared relative offset - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated relative concat-buffer offset for later string appenders + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the hex2bin spill slots before returning the decoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the decoded string emitter.instruction("ret"); // return the decoded string through the standard x86_64 string result registers } diff --git a/src/codegen_support/runtime/strings/html_entity_decode.rs b/src/codegen_support/runtime/strings/html_entity_decode.rs index 16323b7f12..e488041189 100644 --- a/src/codegen_support/runtime/strings/html_entity_decode.rs +++ b/src/codegen_support/runtime/strings/html_entity_decode.rs @@ -7,10 +7,12 @@ //! //! Key details: //! - HTML escaping helpers are emitted scanners that must keep entity tables and quote handling in sync with PHP semantics. +//! - Entity decoding never grows the payload, so the source length is reserved through +//! `__rt_concat_reserve` before the first store; inputs beyond the 64 KiB concat scratch +//! buffer fall back to heap storage instead of running off the end of it. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `runtime helper for ARM64`. /// @@ -20,7 +22,11 @@ use crate::codegen_support::abi; /// non-matching bytes are copied as-is. /// /// Input: x1 = string pointer, x2 = string length (ElephC string convention). -/// Output: x1 = result pointer in concat_buf, x2 = result length. +/// Output: x1 = result pointer, x2 = result length. +/// +/// Reserves the (never-exceeded) source length through `__rt_concat_reserve` — concat scratch +/// while it fits, owned heap storage otherwise — and finishes through `__rt_concat_publish`. +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. pub fn emit_html_entity_decode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_html_entity_decode_linux_x86_64(emitter); @@ -31,12 +37,16 @@ pub fn emit_html_entity_decode(emitter: &mut Emitter) { emitter.comment("--- runtime: html_entity_decode ---"); emitter.label_global("__rt_html_entity_decode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case (unchanged-length) decoded result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the html_entity_decode helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x0, x2"); // entity decoding never grows the payload, so the source length bounds the result + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_hed_loop"); @@ -160,9 +170,9 @@ pub fn emit_html_entity_decode(emitter: &mut Emitter) { emitter.label("__rt_hed_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the html_entity_decode helper frame emitter.instruction("ret"); // return } @@ -174,21 +184,27 @@ pub fn emit_html_entity_decode(emitter: &mut Emitter) { /// /// ABI contract: /// - Input: rax = string pointer, rdx = string length (ElephC convention) -/// - Output: rax = result pointer (concat_buf), rdx = result length -/// - Clobbers: r8-r11, rcx, rsi, rdx; advances `_concat_off` by the produced length +/// - Output: rax = result pointer, rdx = result length +/// - Reserves the (never-exceeded) source length through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`, so long inputs use owned heap storage instead +/// of running off the end of the 64 KiB concat scratch buffer. fn emit_html_entity_decode_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: html_entity_decode ---"); emitter.label_global("__rt_html_entity_decode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before decoding HTML entities back into plain bytes - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the decoded string begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed entity-encoded input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + // -- reserve the worst-case (unchanged-length) decoded result before writing anything -- + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("mov rax, rdx"); // entity decoding never grows the payload, so the source length bounds the result + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the decoded string begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed entity-encoded input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_hed_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied or decoded into concat storage @@ -309,11 +325,11 @@ fn emit_html_entity_decode_linux_x86_64(emitter: &mut Emitter) { // -- finalize -- emitter.label("__rt_hed_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after decoding the full input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the decoded string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after decoding the full input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the decoded string length emitter.instruction("sub rdx, r8"); // compute the decoded string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that html_entity_decode() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced decoded-string length - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the HTML-entity decode pass - emitter.instruction("ret"); // return the concat-backed decoded string in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the html_entity_decode spill slots before returning the decoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the decoded string + emitter.instruction("ret"); // return the decoded string in the standard x86_64 string result registers } diff --git a/src/codegen_support/runtime/strings/htmlspecialchars.rs b/src/codegen_support/runtime/strings/htmlspecialchars.rs index 95ef3d1204..e0c605dbab 100644 --- a/src/codegen_support/runtime/strings/htmlspecialchars.rs +++ b/src/codegen_support/runtime/strings/htmlspecialchars.rs @@ -7,10 +7,12 @@ //! //! Key details: //! - HTML escaping helpers are emitted scanners that must keep entity tables and quote handling in sync with PHP semantics. +//! - The worst-case `6 * len` expansion (`"` / `'`) is reserved through +//! `__rt_concat_reserve` before the first store, so long inputs fall back to heap storage +//! instead of running off the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `__rt_htmlspecialchars` runtime helper for ARM64. /// @@ -19,8 +21,12 @@ use crate::codegen_support::abi; /// /// # ABI (ARM64) /// - **Input**: `x1` = source string pointer, `x2` = source byte length -/// - **Output**: `x1` = result pointer in `_concat_buf`, `x2` = result byte length -/// - Writes into `_concat_buf`, advances `_concat_off` by the produced length. +/// - **Output**: `x1` = result pointer, `x2` = result byte length +/// - Reserves the worst-case `6 * len` expansion through `__rt_concat_reserve` (concat scratch +/// while it fits, owned heap storage otherwise) and finishes through `__rt_concat_publish`. +/// - Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// - A wrapped `6 * len` product reports PHP's allocation-overflow fatal through +/// `__rt_alloc_overflow` instead of reserving a too-small destination. /// /// # PHP compatibility /// Single-quote escape uses `'` (numeric entity) to match PHP's default `ENT_QUOTES` behavior. @@ -34,12 +40,19 @@ pub fn emit_htmlspecialchars(emitter: &mut Emitter) { emitter.comment("--- runtime: htmlspecialchars ---"); emitter.label_global("__rt_htmlspecialchars"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case six-bytes-per-input-byte entity expansion before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the htmlspecialchars helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x9, #6"); // worst-case entity expansion factor (`"` / `'`) + emitter.instruction("umulh x10, x2, x9"); // capture the high half of the 6 * length product + emitter.instruction("cbnz x10, __rt_htmlsc_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x0, x2, x9"); // compute the worst-case escaped result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_htmlsc_loop"); @@ -144,10 +157,14 @@ pub fn emit_htmlspecialchars(emitter: &mut Emitter) { emitter.label("__rt_htmlsc_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the htmlspecialchars helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_htmlsc_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the `__rt_htmlspecialchars` runtime helper for Linux x86_64. @@ -157,21 +174,28 @@ pub fn emit_htmlspecialchars(emitter: &mut Emitter) { /// /// # ABI (x86_64 System V) /// - **Input**: `rax` = source string pointer, `rdx` = source byte length -/// - **Output**: `rax` = result pointer in `_concat_buf`, `rdx` = result byte length -/// - Writes into `_concat_buf`, advances `_concat_off` by the produced length. +/// - **Output**: `rax` = result pointer, `rdx` = result byte length +/// - Reserves the worst-case `6 * len` expansion through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`, so long inputs use owned heap storage instead +/// of running off the end of the 64 KiB concat scratch buffer. fn emit_htmlspecialchars_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: htmlspecialchars ---"); emitter.label_global("__rt_htmlspecialchars"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before expanding HTML-sensitive characters - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the escaped HTML string begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + // -- reserve the worst-case six-bytes-per-input-byte entity expansion before writing anything -- + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("imul rax, rdx, 6"); // compute the worst-case escaped result size as 6 * source length + emitter.instruction("jo __rt_htmlsc_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the escaped HTML string begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_htmlsc_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied into concat storage @@ -259,11 +283,15 @@ fn emit_htmlspecialchars_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_htmlsc_loop_linux_x86_64"); // continue escaping the remaining source bytes after expanding one greater-than sign emitter.label("__rt_htmlsc_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after escaping the full input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the escaped string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after escaping the full input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the escaped string length emitter.instruction("sub rdx, r8"); // compute the escaped string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that htmlspecialchars() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced escaped-string length - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the HTML-escape expansion - emitter.instruction("ret"); // return the concat-backed escaped string in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the htmlspecialchars spill slots before returning the escaped string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the escaped string + emitter.instruction("ret"); // return the escaped string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_htmlsc_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/mod.rs b/src/codegen_support/runtime/strings/mod.rs index 04a8ff7ee5..fa264dbd7e 100644 --- a/src/codegen_support/runtime/strings/mod.rs +++ b/src/codegen_support/runtime/strings/mod.rs @@ -10,15 +10,19 @@ mod itoa; mod concat; +mod concat_scratch; mod ftoa; +mod php_num_scan; mod str_eq; mod str_loose_eq; mod str_to_number; mod str_to_int; +mod str_to_int_base; mod number_format; mod atoi; mod grapheme_strrev; mod strcopy; +mod str_inc_dec; mod str_persist; mod strtolower; mod strtoupper; @@ -27,11 +31,15 @@ mod ltrim; mod rtrim; mod strpos; mod strrpos; +mod stripos; +mod strripos; mod str_repeat; mod strrev; mod chr; mod strcmp; mod strcasecmp; +mod strncmp; +mod strncasecmp; mod str_starts_with; mod str_ends_with; mod str_replace; @@ -41,14 +49,24 @@ mod implode_bool; mod implode_int; mod ucwords; mod str_ireplace; +mod substr_count; mod substr_replace; mod str_pad; mod str_split; +mod str_word_count; mod addslashes; mod stripslashes; mod nl2br; +mod base_convert; +mod chunk_split; +mod count_chars; +mod strtr; +mod quotemeta; +mod quoted_printable_encode; mod wordwrap; mod bin2hex; +mod base_to_number; +mod dec_to_base; mod hex2bin; mod inet_ntop; mod inet_pton; @@ -87,7 +105,8 @@ pub use itoa::emit_itoa; /// Emit integer-to-string conversion helper. pub use concat::emit_concat; /// Emit string concatenation helper. -pub use ftoa::emit_ftoa; +pub use ftoa::{emit_ftoa, emit_ftoa_repr}; +pub use php_num_scan::emit_php_num_scan; /// Emit float-to-string conversion helper. pub use str_eq::emit_str_eq; /// Emit case-sensitive string equality check. @@ -98,6 +117,7 @@ pub use str_to_number::emit_str_to_number; pub use str_to_number::emit_str_looks_like_int_for_coercion; /// Emit string-to-number conversion helper. pub use str_to_int::emit_str_to_int; +pub use str_to_int_base::emit_str_to_int_base; /// Emit PHP string-to-integer cast helper. pub use number_format::emit_number_format; /// Emit number formatting helper. @@ -105,8 +125,11 @@ pub use atoi::emit_atoi; /// Emit ASCII-to-integer conversion. pub use strcopy::emit_strcopy; /// Emit string copy helper. +pub use concat_scratch::emit_concat_scratch; pub use str_persist::emit_str_persist; /// Emit string persistence helper. +pub use str_inc_dec::{emit_mixed_inc_dec, emit_str_inc_dec}; +/// Emit PHP's `++`/`--` on a string value and its boxed dispatch entry point. pub use strtolower::emit_strtolower; /// Emit lowercase string conversion. pub use strtoupper::emit_strtoupper; @@ -121,6 +144,10 @@ pub use strpos::emit_strpos; /// Emit string position lookup (first occurrence). pub use strrpos::emit_strrpos; /// Emit string position lookup (last occurrence). +pub use stripos::emit_stripos; +/// Emit case-insensitive string position lookup (first occurrence). +pub use strripos::emit_strripos; +/// Emit case-insensitive string position lookup (last occurrence). pub use str_repeat::emit_str_repeat; /// Emit string repeat helper. pub use strrev::emit_strrev; @@ -132,6 +159,10 @@ pub use chr::emit_chr; pub use strcmp::emit_strcmp; /// Emit case-sensitive string comparison. pub use strcasecmp::emit_strcasecmp; +/// Emit length-limited case-sensitive string comparison. +pub use strncmp::emit_strncmp; +/// Emit length-limited case-insensitive string comparison. +pub use strncasecmp::emit_strncasecmp; /// Emit case-insensitive string comparison. pub use str_starts_with::emit_str_starts_with; /// Emit check for string prefix match. @@ -151,6 +182,8 @@ pub use ucwords::emit_ucwords; /// Emit uppercase-words helper. pub use str_ireplace::emit_str_ireplace; /// Emit case-insensitive string replace. +pub use substr_count::emit_substr_count; +/// Emit the non-overlapping substring occurrence counter. pub use substr_replace::emit_substr_replace; /// Emit substring replace helper. pub use str_pad::emit_str_pad; @@ -162,10 +195,28 @@ pub use addslashes::emit_addslashes; pub use stripslashes::emit_stripslashes; /// Emit stripslashes unescaping helper. pub use nl2br::emit_nl2br; +/// Emit the base_convert numeral re-renderer. +pub use base_convert::emit_base_convert; +/// Emit the chunk_split fixed-length splitter. +pub use chunk_split::emit_chunk_split; +/// Emit the count_chars byte-frequency tally. +pub use count_chars::emit_count_chars; +/// Emit the strtr pairwise and replacement-pair translators. +pub use strtr::emit_strtr; +/// Emit the quotemeta regular-expression metacharacter escaper. +pub use quotemeta::emit_quotemeta; +/// Emit the quoted_printable_encode MIME transfer encoder. +pub use quoted_printable_encode::emit_quoted_printable_encode; +/// Emit the str_word_count word scanner. +pub use str_word_count::emit_str_word_count; /// Emit newline to `
` conversion. pub use wordwrap::emit_wordwrap; /// Emit wordwrap helper. pub use bin2hex::emit_bin2hex; +/// Emit the shared unsigned integer-to-base renderer used by dechex/decbin/decoct. +pub use dec_to_base::emit_dec_to_base; +/// Emit the shared base-digit parser used by hexdec/bindec/octdec. +pub use base_to_number::emit_base_to_number; /// Emit binary-to-hexadecimal encoding. pub use hex2bin::emit_hex2bin; /// Emit hexadecimal-to-binary decoding. @@ -191,6 +242,9 @@ pub use base64_encode::emit_base64_encode; /// Emit Base64 encoding helper. pub use base64_decode::emit_base64_decode; /// Emit Base64 decoding helper. +/// Re-export the php-src reverse-table sentinels so `runtime::data::fixed` builds +/// `_b64_decode_tbl` from the exact classification `__rt_base64_decode` reads back. +pub use base64_decode::{B64_DECODE_INVALID, B64_DECODE_SKIP, B64_DECODE_WHITESPACE}; pub use sprintf::emit_sprintf; pub use vsprintf::emit_vsprintf; /// Emit sprintf formatting helper. diff --git a/src/codegen_support/runtime/strings/nl2br.rs b/src/codegen_support/runtime/strings/nl2br.rs index 090232b517..037ce6ee17 100644 --- a/src/codegen_support/runtime/strings/nl2br.rs +++ b/src/codegen_support/runtime/strings/nl2br.rs @@ -7,20 +7,27 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - The worst-case `7 * len` expansion (an all-newline input becomes `
\n` per byte) is +//! reserved through `__rt_concat_reserve` before the first store, so long inputs fall back to +//! heap storage instead of running off the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `__rt_nl2br` runtime helper for the nl2br PHP builtin. /// /// Dispatches to the platform-specific implementation (x86_64 Linux or ARM64). /// On ARM64 the helper reads the input string from x1 (ptr) and x2 (len), scans each /// byte, and inserts the literal `
` before every `0x0A` newline. The result -/// pointer/length are returned in x1/x2. The concat-buffer write offset (`_concat_off`) -/// is advanced by the total bytes written. +/// pointer/length are returned in x1/x2. /// -/// Traps: none. Clobbers: x6-x13, x8. Preserves: x0-x5, x29, x30. +/// Reserves the worst-case `7 * len` expansion through `__rt_concat_reserve` (concat scratch +/// while it fits, owned heap storage otherwise) and finishes through `__rt_concat_publish`, +/// which advances `_concat_off` only for scratch-backed results. +/// +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// A wrapped `7 * len` product reports PHP's allocation-overflow fatal through +/// `__rt_alloc_overflow` instead of reserving a too-small destination. pub fn emit_nl2br(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_nl2br_linux_x86_64(emitter); @@ -31,11 +38,19 @@ pub fn emit_nl2br(emitter: &mut Emitter) { emitter.comment("--- runtime: nl2br ---"); emitter.label_global("__rt_nl2br"); - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case seven-bytes-per-input-byte expansion before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the nl2br helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x9, #7"); // worst-case expansion factor for an all-newline input (`
` plus the newline) + emitter.instruction("umulh x10, x2, x9"); // capture the high half of the 7 * length product + emitter.instruction("cbnz x10, __rt_nl2br_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x0, x2, x9"); // compute the worst-case expanded result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the expanded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining count emitter.label("__rt_nl2br_loop"); @@ -64,32 +79,43 @@ pub fn emit_nl2br(emitter: &mut Emitter) { emitter.label("__rt_nl2br_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the nl2br helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_nl2br_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of the `__rt_nl2br` runtime helper. /// /// Reads the input string from rax (ptr) and rdx (len). Scans each byte and inserts /// the literal `
` before every `0x0A` newline. The result pointer/length are -/// returned in rax/rdx. The concat-buffer write offset (`_concat_off`) is advanced -/// by the total bytes written. +/// returned in rax/rdx. /// -/// Clobbers: r8-r11, rcx, rsi, rdx. Preserves: rbx, rbp, r12-r15. +/// Reserves the worst-case `7 * len` expansion through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`, so long inputs use owned heap storage instead +/// of running off the end of the 64 KiB concat scratch buffer. fn emit_nl2br_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: nl2br ---"); emitter.label_global("__rt_nl2br"); - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before expanding newline bytes into HTML break tags - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the nl2br() result begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + // -- reserve the worst-case seven-bytes-per-input-byte expansion before writing anything -- + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("imul rax, rdx, 7"); // compute the worst-case expanded result size as 7 * source length + emitter.instruction("jo __rt_nl2br_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the expanded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the nl2br() result begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_nl2br_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied into concat storage @@ -118,11 +144,15 @@ fn emit_nl2br_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_nl2br_loop_linux_x86_64"); // continue scanning the remaining source bytes until the input string is exhausted emitter.label("__rt_nl2br_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after nl2br() finishes expanding the input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the produced string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after nl2br() finishes expanding the input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the produced string length emitter.instruction("sub rdx, r8"); // compute the produced string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that nl2br() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced string length that nl2br() just materialized - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the nl2br() expansion - emitter.instruction("ret"); // return the concat-backed nl2br() result in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the nl2br spill slots before returning the expanded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the expanded string + emitter.instruction("ret"); // return the nl2br() result in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_nl2br_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/number_format.rs b/src/codegen_support/runtime/strings/number_format.rs index 6bfb35048a..140c665f69 100644 --- a/src/codegen_support/runtime/strings/number_format.rs +++ b/src/codegen_support/runtime/strings/number_format.rs @@ -7,92 +7,175 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - The result is bounded by the fixed `RAW_BUFFER_BYTES` snprintf buffer plus its grouping +//! separators, so `GROUPED_RESULT_BYTES` is reserved through `__rt_concat_reserve` before the +//! first store. That keeps a format that lands near the end of the 64 KiB concat scratch +//! buffer from spilling past it into the adjacent BSS globals. +//! - `$decimals` is a PHP integer, not a digit. A negative value is not an error in PHP: the +//! number is pre-rounded to that power of ten (half away from zero, on the magnitude) and then +//! formatted with no decimals, so `number_format(1234.5678, -1)` is `"1,230"`. The precision +//! actually handed to `snprintf` is therefore always in `0..=MAX_FORMAT_PRECISION` and is +//! written as two ASCII digits; the previous single-digit `'0' + N` shortcut turned `-1` into +//! `"%./f"` and `10` into `"%.:f"`, which is where the `"/f"` garbage came from. +//! - `snprintf` returns the length it *would* have written, so that return value is clamped to +//! the buffer capacity before the grouping pass copies from it. Without both the wider buffer +//! and that clamp, a wide number read past the old 48-byte buffer into the adjacent frame +//! slots and rendered the trailing digits from whatever was there. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; +/// Bytes of the fixed on-stack buffer `snprintf` renders the ungrouped number into. +/// +/// A `double` needs at most 309 integer digits, so 384 bytes holds the widest possible +/// integer part plus the decimal point plus `MAX_FORMAT_PRECISION` decimals without ever +/// truncating. +const RAW_BUFFER_BYTES: i64 = 384; + +/// Bytes reserved through `__rt_concat_reserve` for the grouped result. +/// +/// The widest raw render plus one thousands separator per three integer digits. +const GROUPED_RESULT_BYTES: i64 = 512; + +/// Highest `$decimals` value `snprintf` is asked for. +/// +/// Two ASCII precision digits allow `0..=99`; the cap keeps the widest possible render +/// (309 integer digits + `.` + this many decimals) inside `RAW_BUFFER_BYTES`, and the raw +/// length is clamped again after `snprintf` returns as a belt-and-braces bound. +const MAX_FORMAT_PRECISION: i64 = 40; + /// Emits the `__rt_number_format` runtime helper. /// /// Formats a floating-point number with configurable decimal places and separators, -/// writing the result into the concat buffer. Dispatches to target-specific implementations. +/// writing the result into storage reserved through `__rt_concat_reserve` and publishing the +/// written length through `__rt_concat_publish`. Dispatches to target-specific implementations. /// /// Input registers (ARM64): `d0` = number, `x1` = decimals, `x2` = dec_point char, `x3` = thousands_sep (0=none) /// Output registers (ARM64): `x1` = string pointer, `x2` = string length /// Input registers (x86_64 SysV): `xmm0` = number, `rdi` = decimals, `rsi` = dec_point, `rdx` = thousands_sep /// Output registers (x86_64 SysV): `rax` = string pointer, `rdx` = string length /// -/// Stack frame layout (ARM64, 128 bytes): -/// `[sp+0..47]` snprintf buffer (48 bytes) -/// `[sp+64..68]` format string `"%.Nf\0"` +/// Stack frame layout (ARM64, 512 bytes): +/// `[sp+48]` pre-round magnitude scratch (negative `$decimals` only) +/// `[sp+56]` pre-round sign flag (negative `$decimals` only) +/// `[sp+64..69]` format string `"%.NNf\0"` /// `[sp+72]` result start ptr /// `[sp+80]` raw snprintf length /// `[sp+88]` number (double) /// `[sp+96]` decimals -/// `[sp+100]` dec_point char -/// `[sp+104]` thousands_sep char +/// `[sp+104]` dec_point char (one byte) +/// `[sp+105]` thousands_sep char (one byte) /// `[sp+112]` saved x29, x30 +/// `[sp+128..511]` snprintf buffer (`RAW_BUFFER_BYTES`) pub fn emit_number_format(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_number_format_linux_x86_64(emitter); return; } - // Stack frame layout (128 bytes): - // [sp+0..47] snprintf buffer (48 bytes) - // [sp+64..68] format string "%.Nf\0" + // Stack frame layout (512 bytes): + // [sp+48] pre-round magnitude / scale scratch (negative $decimals only) + // [sp+56] pre-round sign flag (negative $decimals only) + // [sp+64..69] format string "%.NNf\0" // [sp+72] result start ptr // [sp+80] raw_len // [sp+88] number (d0) // [sp+96] decimals - // [sp+100] dec_point char - // [sp+104] thousands_sep char + // [sp+104] dec_point char (one byte) + // [sp+105] thousands_sep char (one byte) // [sp+112] saved x29, x30 + // [sp+128..511] snprintf buffer (RAW_BUFFER_BYTES) emitter.blank(); emitter.comment("--- runtime: number_format ---"); emitter.label_global("__rt_number_format"); - // -- set up stack frame (128 bytes) -- - emitter.instruction("sub sp, sp, #128"); // allocate 128 bytes on the stack + // -- set up stack frame (512 bytes) -- + emitter.instruction("sub sp, sp, #512"); // allocate the number_format() frame: metadata low, raw snprintf buffer high emitter.instruction("stp x29, x30, [sp, #112]"); // save frame pointer and return address emitter.instruction("add x29, sp, #112"); // establish new frame pointer // -- save input arguments -- emitter.instruction("str x1, [sp, #96]"); // save decimals count emitter.instruction("str d0, [sp, #88]"); // save the floating-point number - emitter.instruction("str x2, [sp, #100]"); // save decimal point character - emitter.instruction("str x3, [sp, #104]"); // save thousands separator character + emitter.instruction("strb w2, [sp, #104]"); // save decimal point character as a byte so it cannot overlap the decimals slot + emitter.instruction("strb w3, [sp, #105]"); // save thousands separator character as its own byte + + // -- negative $decimals: pre-round the magnitude to that power of ten, then use no decimals -- + emitter.instruction("ldr x9, [sp, #96]"); // load the requested decimals count + emitter.instruction("cmp x9, #0"); // is the caller asking for fewer significant digits? + emitter.instruction("b.ge __rt_nf_precision_ready"); // a non-negative precision goes straight to snprintf + emitter.instruction("ldr d0, [sp, #88]"); // reload the caller's number + emitter.instruction("fabs d1, d0"); // PHP rounds the magnitude, then reapplies the sign + emitter.instruction("str d1, [sp, #48]"); // park the magnitude across the libm calls + emitter.instruction("fcmp d0, #0.0"); // was the caller's number negative? + emitter.instruction("cset x10, mi"); // remember the sign so it can be restored after rounding + emitter.instruction("str x10, [sp, #56]"); // park the sign flag across the libm calls + emitter.instruction("neg x9, x9"); // the power of ten to round to is -$decimals + emitter.instruction("scvtf d1, x9"); // pass that power as the libm pow() exponent + emitter.instruction("mov x10, #10"); // the rounding base is ten + emitter.instruction("scvtf d0, x10"); // pass the base as the libm pow() mantissa argument + emitter.bl_c("pow"); // d0 = 10 ** -$decimals + emitter.instruction("ldr d1, [sp, #48]"); // reload the parked magnitude + emitter.instruction("fdiv d1, d1, d0"); // scale the magnitude down to the requested precision + emitter.instruction("str d0, [sp, #48]"); // park the scale for the rescale step + emitter.instruction("fmov d2, #0.5"); // half-away-from-zero rounding adds a half before flooring + emitter.instruction("fadd d0, d1, d2"); // bias the scaled magnitude for PHP_ROUND_HALF_UP + emitter.instruction("frintm d0, d0"); // floor the biased magnitude, matching PHP on exact halves + emitter.instruction("fcmp d0, #0.0"); // did the requested precision round the value away entirely? + emitter.instruction("b.eq __rt_nf_precision_zero"); // PHP prints a plain "0", never "-0", in that case + emitter.instruction("ldr d1, [sp, #48]"); // reload the parked scale + emitter.instruction("fmul d0, d0, d1"); // rescale the rounded magnitude back up + emitter.instruction("ldr x10, [sp, #56]"); // reload the parked sign flag + emitter.instruction("cbz x10, __rt_nf_precision_zero"); // a positive number needs no sign restored + emitter.instruction("fneg d0, d0"); // restore the caller's sign on the rounded magnitude + emitter.label("__rt_nf_precision_zero"); + emitter.instruction("str d0, [sp, #88]"); // publish the pre-rounded number for snprintf + emitter.instruction("str xzr, [sp, #96]"); // a pre-rounded value is formatted with no decimals + emitter.label("__rt_nf_precision_ready"); - // -- build format string "%.Nf" at [sp+64] -- + // -- build format string "%.NNf" at [sp+64] -- emitter.instruction("mov w9, #37"); // ASCII '%' emitter.instruction("strb w9, [sp, #64]"); // write '%' to format string emitter.instruction("mov w9, #46"); // ASCII '.' emitter.instruction("strb w9, [sp, #65]"); // write '.' to format string - emitter.instruction("ldr x9, [sp, #96]"); // load decimals count - emitter.instruction("add w9, w9, #48"); // convert to ASCII digit ('0' + N) - emitter.instruction("strb w9, [sp, #66]"); // write decimal count digit + emitter.instruction("ldr x9, [sp, #96]"); // load the now non-negative decimals count + emitter.instruction(&format!("cmp x9, #{}", MAX_FORMAT_PRECISION)); // cap the precision at what the raw buffer can hold + emitter.instruction("b.le __rt_nf_precision_capped"); // keep the requested precision when it already fits + emitter.instruction(&format!("mov x9, #{}", MAX_FORMAT_PRECISION)); // clamp an over-wide precision to the buffer limit + emitter.label("__rt_nf_precision_capped"); + emitter.instruction("mov x10, #10"); // split the precision into two ASCII digits + emitter.instruction("udiv x11, x9, x10"); // x11 = tens digit of the precision + emitter.instruction("msub x12, x11, x10, x9"); // x12 = units digit of the precision + emitter.instruction("add w11, w11, #48"); // convert the tens digit to ASCII + emitter.instruction("strb w11, [sp, #66]"); // write the tens precision digit + emitter.instruction("add w12, w12, #48"); // convert the units digit to ASCII + emitter.instruction("strb w12, [sp, #67]"); // write the units precision digit emitter.instruction("mov w9, #102"); // ASCII 'f' - emitter.instruction("strb w9, [sp, #67]"); // write 'f' format specifier - emitter.instruction("strb wzr, [sp, #68]"); // null-terminate the format string + emitter.instruction("strb w9, [sp, #68]"); // write 'f' format specifier + emitter.instruction("strb wzr, [sp, #69]"); // null-terminate the format string // -- call snprintf(buf, 48, fmt, d0) -- - emitter.instruction("add x0, sp, #0"); // x0 = output buffer at start of stack frame - emitter.instruction("mov x1, #48"); // buffer size = 48 bytes + emitter.instruction("add x0, sp, #128"); // x0 = the raw snprintf buffer above the frame metadata + emitter.instruction(&format!("mov x1, #{}", RAW_BUFFER_BYTES)); // bound the raw snprintf buffer emitter.instruction("add x2, sp, #64"); // x2 = format string pointer emitter.instruction("ldr d0, [sp, #88]"); // reload the float value emitter.instruction("str d0, [sp, #-16]!"); // push double for variadic ABI, adjust sp emitter.bl_c("snprintf"); // call snprintf; returns char count in x0 emitter.instruction("add sp, sp, #16"); // pop the variadic argument from stack + emitter.instruction(&format!("cmp x0, #{}", RAW_BUFFER_BYTES - 1)); // snprintf reports the untruncated length, which may exceed the buffer + emitter.instruction("b.le __rt_nf_raw_len_ok"); // keep the reported length when it actually fits + emitter.instruction(&format!("mov x0, #{}", RAW_BUFFER_BYTES - 1)); // never scan past the raw buffer for a truncated result + emitter.label("__rt_nf_raw_len_ok"); emitter.instruction("str x0, [sp, #80]"); // save raw string length - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current concat_buf write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x10, x7, x8"); // compute destination pointer + // -- reserve bounded destination storage (48 raw bytes plus grouping separators) -- + emitter.instruction(&format!("mov x0, #{}", GROUPED_RESULT_BYTES)); // the raw snprintf buffer plus its thousands separators can never exceed this + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the grouped number + emitter.instruction("mov x10, x0"); // compute destination pointer emitter.instruction("str x10, [sp, #72]"); // save result start pointer // -- scan raw string to find integer part length -- - emitter.instruction("add x11, sp, #0"); // x11 = source ptr (snprintf output) + emitter.instruction("add x11, sp, #128"); // x11 = source ptr (snprintf output) emitter.instruction("ldr x12, [sp, #80]"); // x12 = raw string length emitter.instruction("mov x13, #0"); // x13 = integer part digit count @@ -136,7 +219,7 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("cbz x16, __rt_nf_no_sep"); // skip separator before first digit emitter.instruction("cmp x14, #0"); // check if current group is exhausted emitter.instruction("b.ne __rt_nf_no_sep"); // group not done, no separator yet - emitter.instruction("ldr x9, [sp, #104]"); // load thousands separator char + emitter.instruction("ldrb w9, [sp, #105]"); // load thousands separator char emitter.instruction("cbz x9, __rt_nf_no_sep_reset"); // skip if separator is 0 (none) emitter.instruction("strb w9, [x10], #1"); // write separator to output, advance dest emitter.label("__rt_nf_no_sep_reset"); @@ -157,23 +240,21 @@ pub fn emit_number_format(emitter: &mut Emitter) { emitter.instruction("ldrb w9, [x15], #1"); // load next decimal char, advance source emitter.instruction("cmp w9, #46"); // check if it's '.' (snprintf decimal point) emitter.instruction("b.ne __rt_nf_dec_store"); // if not '.', store as-is - emitter.instruction("ldr x9, [sp, #100]"); // replace with custom decimal point char + emitter.instruction("ldrb w9, [sp, #104]"); // replace with custom decimal point char emitter.label("__rt_nf_dec_store"); emitter.instruction("strb w9, [x10], #1"); // write char to output, advance dest emitter.instruction("sub x12, x12, #1"); // decrement remaining chars emitter.instruction("b __rt_nf_copy_dec"); // continue copying decimal part - // -- finalize: compute length and update concat_off -- + // -- finalize: compute length and publish the written bytes -- emitter.label("__rt_nf_done"); emitter.instruction("ldr x1, [sp, #72]"); // load result start pointer emitter.instruction("sub x2, x10, x1"); // result length = dest_end - dest_start - emitter.instruction("ldr x8, [x6]"); // load current concat_off - emitter.instruction("add x8, x8, x2"); // advance offset by result length - emitter.instruction("str x8, [x6]"); // store updated concat_off + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results // -- restore frame and return -- emitter.instruction("ldp x29, x30, [sp, #112]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #128"); // deallocate stack frame + emitter.instruction("add sp, sp, #512"); // deallocate stack frame emitter.instruction("ret"); // return to caller } @@ -187,30 +268,88 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the raw snprintf buffer, mini format string, and concat-buffer state emitter.instruction("push rbx"); // preserve the concat-buffer destination cursor across the local formatting and copy loops emitter.instruction("push r12"); // preserve the concat-buffer start pointer for the final x86_64 string return pair - emitter.instruction("push r13"); // preserve the concat-offset symbol address across the local formatting and copy loops - emitter.instruction("sub rsp, 104"); // reserve local storage; bumped 96→104 so the four 8-byte saves above + this sub leave rsp 0-mod-16 before the SysV snprintf call below + emitter.instruction("push r13"); // preserve one more callee-saved register so the frame stays 16-byte aligned for the SysV snprintf call + emitter.instruction("sub rsp, 488"); // reserve local storage; the four 8-byte saves above plus this sub leave rsp 0-mod-16 before the SysV snprintf call below emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // preserve the requested decimal count across the intermediate formatting and copy loops emitter.instruction("mov QWORD PTR [rbp - 48], rsi"); // preserve the decimal-separator byte across the intermediate formatting and copy loops emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // preserve the thousands-separator byte across the intermediate formatting and copy loops + emitter.instruction("movsd QWORD PTR [rbp - 128], xmm0"); // park the caller's number so the pre-round libm calls cannot lose it + + // -- negative $decimals: pre-round the magnitude to that power of ten, then use no decimals -- + emitter.instruction("cmp QWORD PTR [rbp - 56], 0"); // is the caller asking for fewer significant digits? + emitter.instruction("jge __rt_nf_precision_ready_linux_x86_64"); // a non-negative precision goes straight to snprintf + emitter.instruction("movq rax, xmm0"); // inspect the raw double bits to split off the sign + emitter.instruction("mov r9, rax"); // copy the bits before the sign bit is cleared + emitter.instruction("shr r9, 63"); // isolate the sign bit as a 0/1 flag + emitter.instruction("mov QWORD PTR [rbp - 32], r9"); // park the sign flag so it can be reapplied after rounding + emitter.instruction("btr rax, 63"); // clear the sign bit to obtain the magnitude, which PHP rounds + emitter.instruction("movq xmm0, rax"); // move the magnitude back into the floating-point register + emitter.instruction("movsd QWORD PTR [rbp - 128], xmm0"); // park the magnitude across the libm calls + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the negative decimals count + emitter.instruction("neg rax"); // the power of ten to round to is -$decimals + emitter.instruction("cvtsi2sd xmm1, rax"); // pass that power as the libm pow() exponent + emitter.instruction("mov eax, 10"); // the rounding base is ten + emitter.instruction("cvtsi2sd xmm0, eax"); // pass the base as the libm pow() mantissa argument + emitter.bl_c("pow"); // xmm0 = 10 ** -$decimals + emitter.instruction("movsd xmm2, QWORD PTR [rbp - 128]"); // reload the parked magnitude + emitter.instruction("divsd xmm2, xmm0"); // scale the magnitude down to the requested precision + emitter.instruction("movsd QWORD PTR [rbp - 128], xmm0"); // park the scale for the rescale step + emitter.instruction("mov eax, 1"); // build 0.5 without an immediate double load + emitter.instruction("cvtsi2sd xmm1, eax"); // xmm1 = 1.0 + emitter.instruction("mov eax, 2"); // the divisor that turns 1.0 into 0.5 + emitter.instruction("cvtsi2sd xmm3, eax"); // xmm3 = 2.0 + emitter.instruction("divsd xmm1, xmm3"); // xmm1 = 0.5, the half-away-from-zero bias + emitter.instruction("addsd xmm2, xmm1"); // bias the scaled magnitude for PHP_ROUND_HALF_UP + emitter.instruction("movapd xmm0, xmm2"); // hand the biased magnitude to libm floor() + emitter.bl_c("floor"); // floor the biased magnitude, matching PHP on exact halves + emitter.instruction("xorpd xmm1, xmm1"); // build a zero to test the rounded magnitude against + emitter.instruction("ucomisd xmm0, xmm1"); // did the requested precision round the value away entirely? + emitter.instruction("je __rt_nf_precision_zero_linux_x86_64"); // PHP prints a plain "0", never "-0", in that case + emitter.instruction("mulsd xmm0, QWORD PTR [rbp - 128]"); // rescale the rounded magnitude back up + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // was the caller's number negative? + emitter.instruction("je __rt_nf_precision_zero_linux_x86_64"); // a positive number needs no sign restored + emitter.instruction("movq rax, xmm0"); // inspect the rounded magnitude bits to restore the sign + emitter.instruction("btc rax, 63"); // flip the sign bit back on for a negative input + emitter.instruction("movq xmm0, rax"); // move the signed rounded value back into the floating-point register + emitter.label("__rt_nf_precision_zero_linux_x86_64"); + emitter.instruction("movsd QWORD PTR [rbp - 128], xmm0"); // publish the pre-rounded number for snprintf + emitter.instruction("mov QWORD PTR [rbp - 56], 0"); // a pre-rounded value is formatted with no decimals + emitter.label("__rt_nf_precision_ready_linux_x86_64"); + emitter.instruction("mov BYTE PTR [rbp - 72], 37"); // seed the mini format string with the leading '%' introducer emitter.instruction("mov BYTE PTR [rbp - 71], 46"); // append the '.' precision introducer to the mini format string - emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the requested decimal count before converting it into the single supported ASCII precision digit - emitter.instruction("add r8b, 48"); // convert the requested decimal count into its single-digit ASCII representation for the mini format string - emitter.instruction("mov BYTE PTR [rbp - 70], r8b"); // append the ASCII precision digit to the mini format string - emitter.instruction("mov BYTE PTR [rbp - 69], 102"); // append the trailing 'f' format type so snprintf renders a fixed-point decimal string - emitter.instruction("mov BYTE PTR [rbp - 68], 0"); // null-terminate the mini format string before handing it to snprintf - emitter.instruction("lea rdi, [rbp - 120]"); // point snprintf at the fixed local raw-decimal buffer that will be post-processed for thousands separators - emitter.instruction("mov esi, 48"); // bound the raw-decimal buffer to 48 bytes before the variadic snprintf call + emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // reload the now non-negative decimal count before converting it to ASCII + emitter.instruction(&format!("cmp r8, {}", MAX_FORMAT_PRECISION)); // cap the precision at what the raw buffer can hold + emitter.instruction("jle __rt_nf_precision_capped_linux_x86_64"); // keep the requested precision when it already fits + emitter.instruction(&format!("mov r8, {}", MAX_FORMAT_PRECISION)); // clamp an over-wide precision to the buffer limit + emitter.label("__rt_nf_precision_capped_linux_x86_64"); + emitter.instruction("mov rax, r8"); // split the precision into two ASCII digits + emitter.instruction("xor rdx, rdx"); // clear the high dividend half before the digit split + emitter.instruction("mov r9, 10"); // the digit-split divisor + emitter.instruction("div r9"); // rax = tens digit, rdx = units digit + emitter.instruction("add al, 48"); // convert the tens digit to ASCII + emitter.instruction("mov BYTE PTR [rbp - 70], al"); // append the tens precision digit to the mini format string + emitter.instruction("mov rax, rdx"); // move the units digit into the byte-addressable accumulator + emitter.instruction("add al, 48"); // convert the units digit to ASCII + emitter.instruction("mov BYTE PTR [rbp - 69], al"); // append the units precision digit to the mini format string + emitter.instruction("mov BYTE PTR [rbp - 68], 102"); // append the trailing 'f' format type so snprintf renders a fixed-point decimal string + emitter.instruction("mov BYTE PTR [rbp - 67], 0"); // null-terminate the mini format string before handing it to snprintf + emitter.instruction("lea rdi, [rbp - 512]"); // point snprintf at the fixed local raw-decimal buffer that will be post-processed for thousands separators + emitter.instruction(&format!("mov esi, {}", RAW_BUFFER_BYTES)); // bound the raw-decimal buffer before the variadic snprintf call emitter.instruction("lea rdx, [rbp - 72]"); // pass the mini format string to snprintf as the fixed-point format pointer + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 128]"); // reload the number, which the pre-round path may have replaced emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register because the formatted number is passed in xmm0 on SysV x86_64 emitter.bl_c("snprintf"); // render the raw fixed-point decimal string into the local snprintf buffer + emitter.instruction(&format!("cmp rax, {}", RAW_BUFFER_BYTES - 1)); // snprintf reports the untruncated length, which may exceed the buffer + emitter.instruction("jle __rt_nf_raw_len_ok_linux_x86_64"); // keep the reported length when it actually fits + emitter.instruction(&format!("mov rax, {}", RAW_BUFFER_BYTES - 1)); // never scan past the raw buffer for a truncated result + emitter.label("__rt_nf_raw_len_ok_linux_x86_64"); emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // preserve the raw snprintf byte count before the thousands-separator pass consumes caller-saved registers - crate::codegen_support::abi::emit_symbol_address(emitter, "r13", "_concat_off"); - emitter.instruction("mov r8, QWORD PTR [r13]"); // load the current concat-buffer write cursor before appending the formatted output - crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_concat_buf"); - emitter.instruction("lea rbx, [r9 + r8]"); // compute the concat-buffer destination cursor where the formatted output will begin - emitter.instruction("mov r12, rbx"); // preserve the concat-buffer start pointer for the final x86_64 string return pair - emitter.instruction("lea r10, [rbp - 120]"); // point at the raw snprintf output buffer before scanning for a leading minus sign and decimal point + emitter.instruction(&format!("mov rax, {}", GROUPED_RESULT_BYTES)); // the raw snprintf buffer plus its thousands separators can never exceed this + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the grouped number + emitter.instruction("mov rbx, rax"); // compute the destination cursor where the formatted output will begin + emitter.instruction("mov r12, rbx"); // preserve the reserved start pointer for the final x86_64 string return pair + emitter.instruction("lea r10, [rbp - 512]"); // point at the raw snprintf output buffer before scanning for a leading minus sign and decimal point emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the raw snprintf byte count before splitting the integer and decimal parts emitter.instruction("movzx eax, BYTE PTR [r10]"); // peek at the first raw formatted byte to detect a leading minus sign emitter.instruction("cmp al, 45"); // is the first raw formatted byte the leading '-' sign? @@ -285,14 +424,12 @@ fn emit_number_format_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_nf_copy_dec_linux_x86_64"); // continue copying the decimal part until every remaining raw byte has been emitted emitter.label("__rt_nf_done_linux_x86_64"); - emitter.instruction("mov rax, r12"); // return the concat-buffer start pointer of the formatted number in the primary x86_64 string result register - emitter.instruction("mov rdx, rbx"); // copy the concat-buffer end cursor so the final formatted-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the formatted-string length from the concat-buffer start and end cursors - emitter.instruction("mov r8, QWORD PTR [r13]"); // reload the old concat-buffer write cursor before publishing the formatted-string append - emitter.instruction("add r8, rdx"); // advance the concat-buffer write cursor by the emitted formatted-string length - emitter.instruction("mov QWORD PTR [r13], r8"); // publish the updated concat-buffer write cursor after appending the formatted number - emitter.instruction("add rsp, 104"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers - emitter.instruction("pop r13"); // restore the saved concat-offset symbol register after the x86_64 number_format() helper finishes + emitter.instruction("mov rax, r12"); // return the reserved start pointer of the formatted number in the primary x86_64 string result register + emitter.instruction("mov rdx, rbx"); // copy the destination end cursor so the final formatted-string length can be derived + emitter.instruction("sub rdx, rax"); // derive the formatted-string length from the destination start and end cursors + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 488"); // release the local raw-buffer and mini-format scratch space before restoring callee-saved registers + emitter.instruction("pop r13"); // restore the callee-saved register kept only to preserve the frame's 16-byte alignment emitter.instruction("pop r12"); // restore the saved concat-buffer start register after the x86_64 number_format() helper finishes emitter.instruction("pop rbx"); // restore the saved concat-buffer destination cursor register after the x86_64 number_format() helper finishes emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the x86_64 formatted string pair diff --git a/src/codegen_support/runtime/strings/php_num_scan.rs b/src/codegen_support/runtime/strings/php_num_scan.rs new file mode 100644 index 0000000000..1b8d61de80 --- /dev/null +++ b/src/codegen_support/runtime/strings/php_num_scan.rs @@ -0,0 +1,330 @@ +//! Purpose: +//! Emits `__rt_php_num_scan`, the runtime implementation of PHP's numeric-string +//! grammar (`_is_numeric_string_ex`). It clips a NUL-terminated C string down to its +//! longest leading numeric run so libc `strtod`/`strtoll` see exactly the bytes PHP +//! would accept, and reports whether the whole string was numeric. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::strings`. +//! - `__rt_str_to_number`, `__rt_str_to_int` and +//! `__rt_str_looks_like_int_for_coercion` (all in +//! `crate::codegen_support::runtime::strings`) after they materialize the PHP string +//! through `__rt_cstr`. +//! +//! Key details: +//! - This is the RUNTIME twin of the compile-time scanner in +//! `crate::optimize::fold::compare::scan_numeric_prefix`; the two must agree byte for +//! byte or a literal and a runtime value give different answers for the same cast. +//! - Grammar: optional PHP whitespace (`' '`, `\t`, `\n`, `\v`, `\f`, `\r`), optional +//! `+`/`-`, decimal digits, an optional `.` with more digits (`12`, `.5`, `5.` all +//! qualify as long as at least one digit was seen), and an optional `e`/`E` exponent +//! that is only consumed when at least one digit follows it. There is NO hexadecimal +//! form, NO underscore separator, and NO `INF`/`NAN` spelling — those are libc +//! `strtod` extensions PHP does not have, which is exactly why the string must be +//! clipped before `strtod` ever sees it. +//! - The clip is written in place into the `__rt_cstr` scratch buffer, which the caller +//! owns until its next `__rt_cstr` call. When there is no numeric prefix at all the +//! run is made empty, so `strtod`/`strtoll` consume nothing and yield PHP's `0`/`0.0`. + +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Emits `__rt_php_num_scan`: clip a C string to PHP's leading numeric run. +/// +/// Input: AArch64 `x0` / x86_64 `rdi` = pointer to a NUL-terminated, writable C string +/// (the `__rt_cstr` scratch buffer). +/// +/// Output: AArch64 `x0` / x86_64 `rax` = pointer to the first byte of the numeric run +/// (past any leading whitespace), NUL-terminated in place at the end of the run; +/// AArch64 `x1` / x86_64 `rdx` = `1` when the string was FULLY numeric (`is_numeric` +/// semantics: only PHP whitespace follows the run), `0` otherwise. +/// +/// The helper is a leaf: it makes no calls and needs no stack frame. +pub fn emit_php_num_scan(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_php_num_scan_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: php_num_scan (PHP numeric-string grammar) ---"); + emitter.label_global("__rt_php_num_scan"); + + emitter.instruction("mov x9, x0"); // x9 = scan cursor over the C string + + // -- skip PHP leading whitespace: ' ' plus the 9..13 control range -- + emitter.label("__rt_pns_ws"); + emitter.instruction("ldrb w10, [x9]"); // load the next candidate whitespace byte + emitter.instruction("cmp w10, #32"); // ASCII space is PHP whitespace + emitter.instruction("b.eq __rt_pns_ws_next"); // skip an allowed leading space + emitter.instruction("sub w11, w10, #9"); // normalize the tab/newline/vtab/formfeed/return range + emitter.instruction("cmp w11, #4"); // bytes 9 through 13 are PHP whitespace + emitter.instruction("b.hi __rt_pns_sign"); // not whitespace: the numeric run starts here + emitter.label("__rt_pns_ws_next"); + emitter.instruction("add x9, x9, #1"); // advance past one whitespace byte + emitter.instruction("b __rt_pns_ws"); // keep skipping leading whitespace + + // -- optional sign -- + emitter.label("__rt_pns_sign"); + emitter.instruction("mov x12, x9"); // x12 = start of the numeric run + emitter.instruction("cmp w10, #43"); // ASCII '+' may lead the run + emitter.instruction("b.eq __rt_pns_sign_skip"); // consume the leading plus + emitter.instruction("cmp w10, #45"); // ASCII '-' may lead the run + emitter.instruction("b.ne __rt_pns_int"); // no sign: start on the integer digits + emitter.label("__rt_pns_sign_skip"); + emitter.instruction("add x9, x9, #1"); // consume the sign byte + + // -- integer digits -- + emitter.label("__rt_pns_int"); + emitter.instruction("mov x13, #0"); // x13 = total digit count seen so far + emitter.label("__rt_pns_int_loop"); + emitter.instruction("ldrb w10, [x9]"); // load the next integer-part byte + emitter.instruction("sub w11, w10, #48"); // normalize to a candidate decimal digit + emitter.instruction("cmp w11, #9"); // verify the decimal digit range + emitter.instruction("b.hi __rt_pns_dot"); // non-digit: try the fractional part + emitter.instruction("add x9, x9, #1"); // consume the digit + emitter.instruction("add x13, x13, #1"); // record one more digit + emitter.instruction("b __rt_pns_int_loop"); // keep consuming integer digits + + // -- optional '.' followed by more digits; "5." counts once a digit was seen -- + emitter.label("__rt_pns_dot"); + emitter.instruction("cmp w10, #46"); // ASCII '.' introduces the fractional part + emitter.instruction("b.ne __rt_pns_after_mantissa"); // no decimal point: mantissa is complete + emitter.instruction("add x14, x9, #1"); // probe cursor just past the '.' + emitter.label("__rt_pns_frac_loop"); + emitter.instruction("ldrb w11, [x14]"); // load the next fractional byte + emitter.instruction("sub w15, w11, #48"); // normalize to a candidate decimal digit + emitter.instruction("cmp w15, #9"); // verify the decimal digit range + emitter.instruction("b.hi __rt_pns_frac_done"); // fractional digits are complete + emitter.instruction("add x14, x14, #1"); // consume the fractional digit + emitter.instruction("add x13, x13, #1"); // record one more digit + emitter.instruction("b __rt_pns_frac_loop"); // keep consuming fractional digits + emitter.label("__rt_pns_frac_done"); + emitter.instruction("cbz x13, __rt_pns_after_mantissa"); // a lone '.' is not part of any numeric run + emitter.instruction("mov x9, x14"); // accept the '.' and its fractional digits + + // -- a run with no digit at all is not numeric -- + emitter.label("__rt_pns_after_mantissa"); + emitter.instruction("cbz x13, __rt_pns_none"); // no digits anywhere: report no numeric prefix + + // -- optional exponent, consumed only when at least one digit follows -- + emitter.instruction("ldrb w10, [x9]"); // load the byte after the mantissa + emitter.instruction("orr w11, w10, #0x20"); // fold it to lowercase ASCII + emitter.instruction("cmp w11, #101"); // lowercase 'e' introduces the exponent + emitter.instruction("b.ne __rt_pns_end"); // no exponent marker: the run ends here + emitter.instruction("add x14, x9, #1"); // probe cursor just past the exponent marker + emitter.instruction("ldrb w11, [x14]"); // load the optional exponent sign + emitter.instruction("cmp w11, #43"); // ASCII '+' may lead the exponent + emitter.instruction("b.eq __rt_pns_exp_sign"); // consume the exponent plus + emitter.instruction("cmp w11, #45"); // ASCII '-' may lead the exponent + emitter.instruction("b.ne __rt_pns_exp_init"); // no exponent sign: start on the digits + emitter.label("__rt_pns_exp_sign"); + emitter.instruction("add x14, x14, #1"); // consume the exponent sign + emitter.label("__rt_pns_exp_init"); + emitter.instruction("mov x15, x14"); // remember where the exponent digits begin + emitter.label("__rt_pns_exp_loop"); + emitter.instruction("ldrb w11, [x14]"); // load the next exponent byte + emitter.instruction("sub w16, w11, #48"); // normalize to a candidate decimal digit + emitter.instruction("cmp w16, #9"); // verify the decimal digit range + emitter.instruction("b.hi __rt_pns_exp_done"); // exponent digits are complete + emitter.instruction("add x14, x14, #1"); // consume the exponent digit + emitter.instruction("b __rt_pns_exp_loop"); // keep consuming exponent digits + emitter.label("__rt_pns_exp_done"); + emitter.instruction("cmp x14, x15"); // did the exponent contain any digit? + emitter.instruction("b.ls __rt_pns_end"); // bare "1e" keeps the 'e' out of the run + emitter.instruction("mov x9, x14"); // accept the exponent + + // -- classify the trailing bytes: only PHP whitespace keeps the string numeric -- + emitter.label("__rt_pns_end"); + emitter.instruction("mov x14, x9"); // x14 = trailing-byte scan cursor + emitter.label("__rt_pns_trail"); + emitter.instruction("ldrb w10, [x14]"); // load the next trailing byte + emitter.instruction("cbz w10, __rt_pns_trail_ok"); // end of string: the whole string was numeric + emitter.instruction("cmp w10, #32"); // ASCII space is allowed after the run + emitter.instruction("b.eq __rt_pns_trail_next"); // keep scanning after an allowed space + emitter.instruction("sub w11, w10, #9"); // normalize the tab/newline/vtab/formfeed/return range + emitter.instruction("cmp w11, #4"); // bytes 9 through 13 are PHP whitespace + emitter.instruction("b.hi __rt_pns_trail_bad"); // any other byte makes the string non-numeric + emitter.label("__rt_pns_trail_next"); + emitter.instruction("add x14, x14, #1"); // advance past one trailing whitespace byte + emitter.instruction("b __rt_pns_trail"); // keep scanning trailing whitespace + emitter.label("__rt_pns_trail_ok"); + emitter.instruction("mov x1, #1"); // report a fully numeric string + emitter.instruction("b __rt_pns_finish"); // clip the run and return + emitter.label("__rt_pns_trail_bad"); + emitter.instruction("mov x1, #0"); // report a leading-numeric-only string + + emitter.label("__rt_pns_finish"); + emitter.instruction("strb wzr, [x9]"); // clip the scratch buffer at the end of the run + emitter.instruction("mov x0, x12"); // return the pointer to the numeric run + emitter.instruction("ret"); // return run pointer (x0) and numeric flag (x1) + + emitter.label("__rt_pns_none"); + emitter.instruction("mov x1, #0"); // no numeric prefix means not a numeric string + emitter.instruction("strb wzr, [x12]"); // make the run empty so strtod/strtoll yield zero + emitter.instruction("mov x0, x12"); // return the empty run pointer + emitter.instruction("ret"); // return run pointer (x0) and numeric flag (x1) +} + +/// Emits the Linux x86_64 variant of `__rt_php_num_scan`. +/// +/// Mirrors the AArch64 grammar exactly using SysV registers. +/// Input: `rdi` = pointer to a NUL-terminated, writable C string. +/// Output: `rax` = pointer to the (in-place clipped) numeric run, `rdx` = fully-numeric flag. +fn emit_php_num_scan_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: php_num_scan (PHP numeric-string grammar) ---"); + emitter.label_global("__rt_php_num_scan"); + + emitter.instruction("mov r8, rdi"); // r8 = scan cursor over the C string + + emitter.label("__rt_pns_ws_x"); + emitter.instruction("movzx ecx, BYTE PTR [r8]"); // load the next candidate whitespace byte + emitter.instruction("cmp cl, 32"); // ASCII space is PHP whitespace + emitter.instruction("je __rt_pns_ws_next_x"); // skip an allowed leading space + emitter.instruction("mov r9d, ecx"); // copy the byte before normalizing the range + emitter.instruction("sub r9d, 9"); // normalize the tab/newline/vtab/formfeed/return range + emitter.instruction("cmp r9d, 4"); // bytes 9 through 13 are PHP whitespace + emitter.instruction("ja __rt_pns_sign_x"); // not whitespace: the numeric run starts here + emitter.label("__rt_pns_ws_next_x"); + emitter.instruction("inc r8"); // advance past one whitespace byte + emitter.instruction("jmp __rt_pns_ws_x"); // keep skipping leading whitespace + + emitter.label("__rt_pns_sign_x"); + emitter.instruction("mov r10, r8"); // r10 = start of the numeric run + emitter.instruction("cmp cl, 43"); // ASCII '+' may lead the run + emitter.instruction("je __rt_pns_sign_skip_x"); // consume the leading plus + emitter.instruction("cmp cl, 45"); // ASCII '-' may lead the run + emitter.instruction("jne __rt_pns_int_x"); // no sign: start on the integer digits + emitter.label("__rt_pns_sign_skip_x"); + emitter.instruction("inc r8"); // consume the sign byte + + emitter.label("__rt_pns_int_x"); + emitter.instruction("xor r11d, r11d"); // r11 = total digit count seen so far + emitter.label("__rt_pns_int_loop_x"); + emitter.instruction("movzx ecx, BYTE PTR [r8]"); // load the next integer-part byte + emitter.instruction("mov r9d, ecx"); // copy the byte before normalizing the range + emitter.instruction("sub r9d, 48"); // normalize to a candidate decimal digit + emitter.instruction("cmp r9d, 9"); // verify the decimal digit range + emitter.instruction("ja __rt_pns_dot_x"); // non-digit: try the fractional part + emitter.instruction("inc r8"); // consume the digit + emitter.instruction("inc r11"); // record one more digit + emitter.instruction("jmp __rt_pns_int_loop_x"); // keep consuming integer digits + + emitter.label("__rt_pns_dot_x"); + emitter.instruction("cmp cl, 46"); // ASCII '.' introduces the fractional part + emitter.instruction("jne __rt_pns_after_mantissa_x"); // no decimal point: mantissa is complete + emitter.instruction("lea rsi, [r8 + 1]"); // probe cursor just past the '.' + emitter.label("__rt_pns_frac_loop_x"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the next fractional byte + emitter.instruction("mov r9d, ecx"); // copy the byte before normalizing the range + emitter.instruction("sub r9d, 48"); // normalize to a candidate decimal digit + emitter.instruction("cmp r9d, 9"); // verify the decimal digit range + emitter.instruction("ja __rt_pns_frac_done_x"); // fractional digits are complete + emitter.instruction("inc rsi"); // consume the fractional digit + emitter.instruction("inc r11"); // record one more digit + emitter.instruction("jmp __rt_pns_frac_loop_x"); // keep consuming fractional digits + emitter.label("__rt_pns_frac_done_x"); + emitter.instruction("test r11, r11"); // did the run contain any digit? + emitter.instruction("jz __rt_pns_after_mantissa_x"); // a lone '.' is not part of any numeric run + emitter.instruction("mov r8, rsi"); // accept the '.' and its fractional digits + + emitter.label("__rt_pns_after_mantissa_x"); + emitter.instruction("test r11, r11"); // did the run contain any digit? + emitter.instruction("jz __rt_pns_none_x"); // no digits anywhere: report no numeric prefix + + emitter.instruction("movzx ecx, BYTE PTR [r8]"); // load the byte after the mantissa + emitter.instruction("mov r9d, ecx"); // copy the byte before case folding + emitter.instruction("or r9d, 32"); // fold it to lowercase ASCII + emitter.instruction("cmp r9d, 101"); // lowercase 'e' introduces the exponent + emitter.instruction("jne __rt_pns_end_x"); // no exponent marker: the run ends here + emitter.instruction("lea rsi, [r8 + 1]"); // probe cursor just past the exponent marker + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the optional exponent sign + emitter.instruction("cmp cl, 43"); // ASCII '+' may lead the exponent + emitter.instruction("je __rt_pns_exp_sign_x"); // consume the exponent plus + emitter.instruction("cmp cl, 45"); // ASCII '-' may lead the exponent + emitter.instruction("jne __rt_pns_exp_init_x"); // no exponent sign: start on the digits + emitter.label("__rt_pns_exp_sign_x"); + emitter.instruction("inc rsi"); // consume the exponent sign + emitter.label("__rt_pns_exp_init_x"); + emitter.instruction("mov r9, rsi"); // remember where the exponent digits begin + emitter.label("__rt_pns_exp_loop_x"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the next exponent byte + emitter.instruction("mov eax, ecx"); // copy the byte before normalizing the range + emitter.instruction("sub eax, 48"); // normalize to a candidate decimal digit + emitter.instruction("cmp eax, 9"); // verify the decimal digit range + emitter.instruction("ja __rt_pns_exp_done_x"); // exponent digits are complete + emitter.instruction("inc rsi"); // consume the exponent digit + emitter.instruction("jmp __rt_pns_exp_loop_x"); // keep consuming exponent digits + emitter.label("__rt_pns_exp_done_x"); + emitter.instruction("cmp rsi, r9"); // did the exponent contain any digit? + emitter.instruction("jbe __rt_pns_end_x"); // bare "1e" keeps the 'e' out of the run + emitter.instruction("mov r8, rsi"); // accept the exponent + + emitter.label("__rt_pns_end_x"); + emitter.instruction("mov rsi, r8"); // rsi = trailing-byte scan cursor + emitter.label("__rt_pns_trail_x"); + emitter.instruction("movzx ecx, BYTE PTR [rsi]"); // load the next trailing byte + emitter.instruction("test cl, cl"); // check for the C-string terminator + emitter.instruction("jz __rt_pns_trail_ok_x"); // end of string: the whole string was numeric + emitter.instruction("cmp cl, 32"); // ASCII space is allowed after the run + emitter.instruction("je __rt_pns_trail_next_x"); // keep scanning after an allowed space + emitter.instruction("mov r9d, ecx"); // copy the byte before normalizing the range + emitter.instruction("sub r9d, 9"); // normalize the tab/newline/vtab/formfeed/return range + emitter.instruction("cmp r9d, 4"); // bytes 9 through 13 are PHP whitespace + emitter.instruction("ja __rt_pns_trail_bad_x"); // any other byte makes the string non-numeric + emitter.label("__rt_pns_trail_next_x"); + emitter.instruction("inc rsi"); // advance past one trailing whitespace byte + emitter.instruction("jmp __rt_pns_trail_x"); // keep scanning trailing whitespace + emitter.label("__rt_pns_trail_ok_x"); + emitter.instruction("mov edx, 1"); // report a fully numeric string + emitter.instruction("jmp __rt_pns_finish_x"); // clip the run and return + emitter.label("__rt_pns_trail_bad_x"); + emitter.instruction("xor edx, edx"); // report a leading-numeric-only string + + emitter.label("__rt_pns_finish_x"); + emitter.instruction("mov BYTE PTR [r8], 0"); // clip the scratch buffer at the end of the run + emitter.instruction("mov rax, r10"); // return the pointer to the numeric run + emitter.instruction("ret"); // return run pointer (rax) and numeric flag (rdx) + + emitter.label("__rt_pns_none_x"); + emitter.instruction("xor edx, edx"); // no numeric prefix means not a numeric string + emitter.instruction("mov BYTE PTR [r10], 0"); // make the run empty so strtod/strtoll yield zero + emitter.instruction("mov rax, r10"); // return the empty run pointer + emitter.instruction("ret"); // return run pointer (rax) and numeric flag (rdx) +} + +#[cfg(test)] +mod tests { + use crate::codegen_support::platform::{Arch, Platform, Target}; + + use super::*; + + /// Verifies both targets emit the whole PHP grammar: whitespace skip, optional sign, + /// mantissa, guarded exponent, and the trailing-whitespace classification. + #[test] + fn test_emit_php_num_scan_covers_full_grammar() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_php_num_scan(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_php_num_scan:\n"), "missing entry point for {:?}", arch); + for fragment in ["__rt_pns_ws", "__rt_pns_sign", "__rt_pns_int", "__rt_pns_frac", "__rt_pns_exp", "__rt_pns_trail", "__rt_pns_none"] { + assert!(asm.contains(fragment), "missing {} for {:?}", fragment, arch); + } + } + } + + /// Verifies the helper is a leaf routine: PHP's grammar is scanned without any call, + /// so callers can invoke it between `__rt_cstr` and `strtod` without a frame of their own. + #[test] + fn test_emit_php_num_scan_is_leaf() { + for arch in [Arch::AArch64, Arch::X86_64] { + let mut emitter = Emitter::new(Target::new(Platform::Linux, arch)); + emit_php_num_scan(&mut emitter); + let asm = emitter.output(); + assert!(!asm.contains(" bl "), "unexpected call for {:?}", arch); + assert!(!asm.contains(" call "), "unexpected call for {:?}", arch); + } + } +} diff --git a/src/codegen_support/runtime/strings/quoted_printable_encode.rs b/src/codegen_support/runtime/strings/quoted_printable_encode.rs new file mode 100644 index 0000000000..7065190a30 --- /dev/null +++ b/src/codegen_support/runtime/strings/quoted_printable_encode.rs @@ -0,0 +1,355 @@ +//! Purpose: +//! Emits the `__rt_quoted_printable_encode` runtime helper assembly, a port of php-src's +//! `php_quot_print_encode` including its 75-column soft-line-break accounting. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - Three classes of byte, in php-src's order: an embedded `CRLF` pair is copied through +//! verbatim and resets the column counter; a control byte, `0x7F`, any byte with the high +//! bit set, `=` itself, or a space DIRECTLY BEFORE a `CR` becomes `=XX`; everything else is +//! copied literally. A trailing space is therefore left alone (nothing follows it), while a +//! trailing tab is a control byte and always becomes `=09`. +//! - The soft break is `=\r\n`, inserted BEFORE the byte that would cross column 75. php-src +//! pre-charges the column counter by 3 and then adds a lookahead allowance for a UTF-8 lead +//! byte (3 more for a 2-byte sequence, 6 for a 3-byte one, 9 for a 4-byte one) so a +//! multi-byte character is not split across the break. Bytes above `0xF4` are never a valid +//! lead byte and php-src's chain simply falls through without a break; that behavior is +//! reproduced exactly rather than "fixed". +//! - Output storage comes from `__rt_concat_reserve`/`__rt_concat_publish`. The reservation is +//! `4 * len + 8`, not php-src's `3 * len`: the measured worst case is 3.1 bytes per input +//! byte (`str_repeat("=", 30)` encodes to 93 bytes) because a soft break adds 3 bytes that +//! php-src's own bound does not account for. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_quoted_printable_encode` runtime helper. +/// +/// ABI (AArch64): `x1` = subject pointer, `x2` = subject byte length; returns `x1`/`x2` = +/// encoded pointer/length. +/// +/// Dispatches to `emit_quoted_printable_encode_linux_x86_64` on x86_64; uses inline AArch64 +/// otherwise. +pub fn emit_quoted_printable_encode(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_quoted_printable_encode_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: quoted_printable_encode ---"); + emitter.label_global("__rt_quoted_printable_encode"); + + // -- reserve worst-case storage before the first byte is classified -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed subject string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the quoted-printable encoder frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the subject pointer and length across the reservation call + emitter.instruction("lsl x0, x2, #2"); // four bytes per input byte covers "=XX" plus its share of soft breaks + emitter.instruction("add x0, x0, #8"); // add slack for a soft break emitted near the very end + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the encoded result + emitter.instruction("mov x9, x0"); // keep the reservation start as the encoded string base + emitter.instruction("mov x10, x0"); // seed the encoded-output write cursor + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed subject pointer and length + emitter.instruction("mov x11, #0"); // lp: php-src's current output column counter + + emitter.label("__rt_qpenc_loop"); + emitter.instruction("cbz x2, __rt_qpenc_done"); // stop once every subject byte has been classified + emitter.instruction("ldrb w12, [x1], #1"); // load the current subject byte and advance the cursor + emitter.instruction("sub x2, x2, #1"); // record that one subject byte has been consumed + emitter.instruction("mov w13, #0"); // php-src reads the NUL terminator past the last byte + emitter.instruction("cbz x2, __rt_qpenc_no_lookahead"); // no following byte, so the lookahead stays zero + emitter.instruction("ldrb w13, [x1]"); // peek the following byte without consuming it + + // -- an embedded CRLF pair is a hard line break and is copied through unchanged -- + emitter.label("__rt_qpenc_no_lookahead"); + emitter.instruction("cmp w12, #13"); // is the current byte a carriage return? + emitter.instruction("b.ne __rt_qpenc_classify"); // only CR can open a hard line break + emitter.instruction("cbz x2, __rt_qpenc_classify"); // a trailing CR has no LF to pair with + emitter.instruction("cmp w13, #10"); // is the following byte a line feed? + emitter.instruction("b.ne __rt_qpenc_classify"); // a lone CR is encoded like any other control byte + emitter.instruction("mov w14, #13"); // copy the carriage return verbatim + emitter.instruction("strb w14, [x10], #1"); // write the carriage return to the output + emitter.instruction("mov w14, #10"); // copy the line feed verbatim + emitter.instruction("strb w14, [x10], #1"); // write the line feed to the output + emitter.instruction("add x1, x1, #1"); // consume the line feed from the subject + emitter.instruction("sub x2, x2, #1"); // record that the paired line feed was consumed + emitter.instruction("mov x11, #0"); // a hard line break restarts the output column + emitter.instruction("b __rt_qpenc_loop"); // classify the next subject byte + + // -- php-src's escape predicate, evaluated in its own order -- + emitter.label("__rt_qpenc_classify"); + emitter.instruction("cmp w12, #32"); // is the byte a C-locale control character? + emitter.instruction("b.lo __rt_qpenc_encode"); // control bytes are always escaped + emitter.instruction("cmp w12, #127"); // is the byte DEL or does it have the high bit set? + emitter.instruction("b.hs __rt_qpenc_encode"); // DEL and every non-ASCII byte are always escaped + emitter.instruction("cmp w12, #61"); // is the byte the '=' escape introducer itself? + emitter.instruction("b.eq __rt_qpenc_encode"); // '=' must be escaped or the output is ambiguous + emitter.instruction("cmp w12, #32"); // is the byte a space? + emitter.instruction("b.ne __rt_qpenc_literal"); // any other printable byte is copied literally + emitter.instruction("cmp w13, #13"); // does a carriage return follow this space? + emitter.instruction("b.eq __rt_qpenc_encode"); // a space at the end of a line must not be stripped in transit + + // -- literal byte: one column, with a soft break when it would cross column 75 -- + emitter.label("__rt_qpenc_literal"); + emitter.instruction("add x11, x11, #1"); // a literal byte occupies exactly one output column + emitter.instruction("cmp x11, #75"); // would this byte still fit on the current line? + emitter.instruction("b.ls __rt_qpenc_literal_write"); // it fits, so no soft line break is needed + emitter.instruction("mov w14, #61"); // a soft line break is written as "=\r\n" + emitter.instruction("strb w14, [x10], #1"); // write the soft-break '=' + emitter.instruction("mov w14, #13"); // continue the soft break with a carriage return + emitter.instruction("strb w14, [x10], #1"); // write the soft-break carriage return + emitter.instruction("mov w14, #10"); // finish the soft break with a line feed + emitter.instruction("strb w14, [x10], #1"); // write the soft-break line feed + emitter.instruction("mov x11, #1"); // the moved byte is the first column of the new line + + emitter.label("__rt_qpenc_literal_write"); + emitter.instruction("strb w12, [x10], #1"); // write the literal subject byte to the output + emitter.instruction("b __rt_qpenc_loop"); // classify the next subject byte + + // -- escaped byte: three columns, plus php-src's UTF-8 lookahead allowance -- + emitter.label("__rt_qpenc_encode"); + emitter.instruction("add x11, x11, #3"); // "=XX" occupies three output columns + emitter.instruction("cmp w12, #127"); // is this an ASCII byte with no continuation bytes to keep together? + emitter.instruction("b.hi __rt_qpenc_lead2"); // a high-bit byte may lead a multi-byte character + emitter.instruction("cmp x11, #75"); // would this escape still fit on the current line? + emitter.instruction("b.hi __rt_qpenc_break"); // break before the escape rather than past column 75 + emitter.instruction("b __rt_qpenc_write"); // the escape fits on the current line + + emitter.label("__rt_qpenc_lead2"); + emitter.instruction("cmp w12, #223"); // is this the lead byte of a two-byte UTF-8 sequence? + emitter.instruction("b.hi __rt_qpenc_lead3"); // no, try the three-byte lead range + emitter.instruction("add x14, x11, #3"); // reserve room for the one continuation byte that follows + emitter.instruction("cmp x14, #75"); // would the whole two-byte character still fit? + emitter.instruction("b.hi __rt_qpenc_break"); // break before the character rather than split it + emitter.instruction("b __rt_qpenc_write"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_lead3"); + emitter.instruction("cmp w12, #239"); // is this the lead byte of a three-byte UTF-8 sequence? + emitter.instruction("b.hi __rt_qpenc_lead4"); // no, try the four-byte lead range + emitter.instruction("add x14, x11, #6"); // reserve room for the two continuation bytes that follow + emitter.instruction("cmp x14, #75"); // would the whole three-byte character still fit? + emitter.instruction("b.hi __rt_qpenc_break"); // break before the character rather than split it + emitter.instruction("b __rt_qpenc_write"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_lead4"); + emitter.instruction("cmp w12, #244"); // is this the lead byte of a four-byte UTF-8 sequence? + emitter.instruction("b.hi __rt_qpenc_write"); // php-src never breaks for a byte above 0xF4 + emitter.instruction("add x14, x11, #9"); // reserve room for the three continuation bytes that follow + emitter.instruction("cmp x14, #75"); // would the whole four-byte character still fit? + emitter.instruction("b.ls __rt_qpenc_write"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_break"); + emitter.instruction("mov w14, #61"); // a soft line break is written as "=\r\n" + emitter.instruction("strb w14, [x10], #1"); // write the soft-break '=' + emitter.instruction("mov w14, #13"); // continue the soft break with a carriage return + emitter.instruction("strb w14, [x10], #1"); // write the soft-break carriage return + emitter.instruction("mov w14, #10"); // finish the soft break with a line feed + emitter.instruction("strb w14, [x10], #1"); // write the soft-break line feed + emitter.instruction("mov x11, #3"); // the moved escape occupies the first three columns of the new line + + emitter.label("__rt_qpenc_write"); + emitter.instruction("mov w14, #61"); // every escape starts with '=' + emitter.instruction("strb w14, [x10], #1"); // write the escape introducer + emitter.instruction("lsr w14, w12, #4"); // isolate the high nibble of the escaped byte + emitter.instruction("cmp w14, #10"); // is the high nibble a decimal digit? + emitter.instruction("b.lo __rt_qpenc_hi_digit"); // digits map onto '0'-'9' + emitter.instruction("add w14, w14, #55"); // map 10-15 onto the uppercase 'A'-'F' php-src uses + emitter.instruction("b __rt_qpenc_hi_write"); // the high nibble is ready to write + + emitter.label("__rt_qpenc_hi_digit"); + emitter.instruction("add w14, w14, #48"); // map 0-9 onto '0'-'9' + + emitter.label("__rt_qpenc_hi_write"); + emitter.instruction("strb w14, [x10], #1"); // write the high hex digit + emitter.instruction("and w14, w12, #0xf"); // isolate the low nibble of the escaped byte + emitter.instruction("cmp w14, #10"); // is the low nibble a decimal digit? + emitter.instruction("b.lo __rt_qpenc_lo_digit"); // digits map onto '0'-'9' + emitter.instruction("add w14, w14, #55"); // map 10-15 onto the uppercase 'A'-'F' php-src uses + emitter.instruction("b __rt_qpenc_lo_write"); // the low nibble is ready to write + + emitter.label("__rt_qpenc_lo_digit"); + emitter.instruction("add w14, w14, #48"); // map 0-9 onto '0'-'9' + + emitter.label("__rt_qpenc_lo_write"); + emitter.instruction("strb w14, [x10], #1"); // write the low hex digit + emitter.instruction("b __rt_qpenc_loop"); // classify the next subject byte + + emitter.label("__rt_qpenc_done"); + emitter.instruction("mov x1, x9"); // return the encoded payload pointer + emitter.instruction("sub x2, x10, x9"); // return the number of encoded bytes actually written + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the quoted-printable encoder frame + emitter.instruction("ret"); // return the encoded string pair +} + +/// Emits the `__rt_quoted_printable_encode` runtime helper for the Linux x86_64 target. +/// +/// ABI (x86_64): `rax` = subject pointer, `rdx` = subject byte length; returns `rax`/`rdx` = +/// encoded pointer/length. +/// +/// Same classification order and column accounting as the AArch64 path. +/// Called exclusively from `emit_quoted_printable_encode` when +/// `emitter.target.arch == Arch::X86_64`. +fn emit_quoted_printable_encode_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: quoted_printable_encode ---"); + emitter.label_global("__rt_quoted_printable_encode"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed subject string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the subject pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the subject pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the subject length across the reservation call + emitter.instruction("mov rax, rdx"); // start the reservation size from the subject length + emitter.instruction("shl rax, 2"); // four bytes per input byte covers "=XX" plus its share of soft breaks + emitter.instruction("add rax, 8"); // add slack for a soft break emitted near the very end + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the encoded result + emitter.instruction("mov r9, rax"); // keep the reservation start as the encoded string base + emitter.instruction("mov r10, rax"); // seed the encoded-output write cursor + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload the borrowed subject pointer into the read cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the subject length into the loop counter + emitter.instruction("xor r11d, r11d"); // lp: php-src's current output column counter + + emitter.label("__rt_qpenc_loop_x86"); + emitter.instruction("test rcx, rcx"); // stop once every subject byte has been classified + emitter.instruction("jz __rt_qpenc_done_x86"); // leave the loop at the end of the subject + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the current subject byte and widen it for comparisons + emitter.instruction("add rsi, 1"); // advance the subject read cursor + emitter.instruction("sub rcx, 1"); // record that one subject byte has been consumed + emitter.instruction("xor r8d, r8d"); // php-src reads the NUL terminator past the last byte + emitter.instruction("test rcx, rcx"); // is there a following byte to peek at? + emitter.instruction("jz __rt_qpenc_no_lookahead_x86"); // no following byte, so the lookahead stays zero + emitter.instruction("movzx r8d, BYTE PTR [rsi]"); // peek the following byte without consuming it + + emitter.label("__rt_qpenc_no_lookahead_x86"); + emitter.instruction("cmp eax, 13"); // is the current byte a carriage return? + emitter.instruction("jne __rt_qpenc_classify_x86"); // only CR can open a hard line break + emitter.instruction("test rcx, rcx"); // is there a following byte at all? + emitter.instruction("jz __rt_qpenc_classify_x86"); // a trailing CR has no LF to pair with + emitter.instruction("cmp r8d, 10"); // is the following byte a line feed? + emitter.instruction("jne __rt_qpenc_classify_x86"); // a lone CR is encoded like any other control byte + emitter.instruction("mov BYTE PTR [r10], 13"); // copy the carriage return verbatim + emitter.instruction("add r10, 1"); // advance the output cursor past the carriage return + emitter.instruction("mov BYTE PTR [r10], 10"); // copy the line feed verbatim + emitter.instruction("add r10, 1"); // advance the output cursor past the line feed + emitter.instruction("add rsi, 1"); // consume the line feed from the subject + emitter.instruction("sub rcx, 1"); // record that the paired line feed was consumed + emitter.instruction("xor r11d, r11d"); // a hard line break restarts the output column + emitter.instruction("jmp __rt_qpenc_loop_x86"); // classify the next subject byte + + emitter.label("__rt_qpenc_classify_x86"); + emitter.instruction("cmp eax, 32"); // is the byte a C-locale control character? + emitter.instruction("jb __rt_qpenc_encode_x86"); // control bytes are always escaped + emitter.instruction("cmp eax, 127"); // is the byte DEL or does it have the high bit set? + emitter.instruction("jae __rt_qpenc_encode_x86"); // DEL and every non-ASCII byte are always escaped + emitter.instruction("cmp eax, 61"); // is the byte the '=' escape introducer itself? + emitter.instruction("je __rt_qpenc_encode_x86"); // '=' must be escaped or the output is ambiguous + emitter.instruction("cmp eax, 32"); // is the byte a space? + emitter.instruction("jne __rt_qpenc_literal_x86"); // any other printable byte is copied literally + emitter.instruction("cmp r8d, 13"); // does a carriage return follow this space? + emitter.instruction("je __rt_qpenc_encode_x86"); // a space at the end of a line must not be stripped in transit + + emitter.label("__rt_qpenc_literal_x86"); + emitter.instruction("add r11, 1"); // a literal byte occupies exactly one output column + emitter.instruction("cmp r11, 75"); // would this byte still fit on the current line? + emitter.instruction("jbe __rt_qpenc_literal_write_x86"); // it fits, so no soft line break is needed + emitter.instruction("mov BYTE PTR [r10], 61"); // write the soft-break '=' + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break '=' + emitter.instruction("mov BYTE PTR [r10], 13"); // write the soft-break carriage return + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break carriage return + emitter.instruction("mov BYTE PTR [r10], 10"); // write the soft-break line feed + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break line feed + emitter.instruction("mov r11, 1"); // the moved byte is the first column of the new line + + emitter.label("__rt_qpenc_literal_write_x86"); + emitter.instruction("mov BYTE PTR [r10], al"); // write the literal subject byte to the output + emitter.instruction("add r10, 1"); // advance the output cursor past the literal byte + emitter.instruction("jmp __rt_qpenc_loop_x86"); // classify the next subject byte + + emitter.label("__rt_qpenc_encode_x86"); + emitter.instruction("add r11, 3"); // "=XX" occupies three output columns + emitter.instruction("cmp eax, 127"); // is this an ASCII byte with no continuation bytes to keep together? + emitter.instruction("ja __rt_qpenc_lead2_x86"); // a high-bit byte may lead a multi-byte character + emitter.instruction("cmp r11, 75"); // would this escape still fit on the current line? + emitter.instruction("ja __rt_qpenc_break_x86"); // break before the escape rather than past column 75 + emitter.instruction("jmp __rt_qpenc_write_x86"); // the escape fits on the current line + + emitter.label("__rt_qpenc_lead2_x86"); + emitter.instruction("cmp eax, 223"); // is this the lead byte of a two-byte UTF-8 sequence? + emitter.instruction("ja __rt_qpenc_lead3_x86"); // no, try the three-byte lead range + emitter.instruction("mov rdx, r11"); // copy the column counter before adding the lookahead allowance + emitter.instruction("add rdx, 3"); // reserve room for the one continuation byte that follows + emitter.instruction("cmp rdx, 75"); // would the whole two-byte character still fit? + emitter.instruction("ja __rt_qpenc_break_x86"); // break before the character rather than split it + emitter.instruction("jmp __rt_qpenc_write_x86"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_lead3_x86"); + emitter.instruction("cmp eax, 239"); // is this the lead byte of a three-byte UTF-8 sequence? + emitter.instruction("ja __rt_qpenc_lead4_x86"); // no, try the four-byte lead range + emitter.instruction("mov rdx, r11"); // copy the column counter before adding the lookahead allowance + emitter.instruction("add rdx, 6"); // reserve room for the two continuation bytes that follow + emitter.instruction("cmp rdx, 75"); // would the whole three-byte character still fit? + emitter.instruction("ja __rt_qpenc_break_x86"); // break before the character rather than split it + emitter.instruction("jmp __rt_qpenc_write_x86"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_lead4_x86"); + emitter.instruction("cmp eax, 244"); // is this the lead byte of a four-byte UTF-8 sequence? + emitter.instruction("ja __rt_qpenc_write_x86"); // php-src never breaks for a byte above 0xF4 + emitter.instruction("mov rdx, r11"); // copy the column counter before adding the lookahead allowance + emitter.instruction("add rdx, 9"); // reserve room for the three continuation bytes that follow + emitter.instruction("cmp rdx, 75"); // would the whole four-byte character still fit? + emitter.instruction("jbe __rt_qpenc_write_x86"); // the whole character fits on the current line + + emitter.label("__rt_qpenc_break_x86"); + emitter.instruction("mov BYTE PTR [r10], 61"); // write the soft-break '=' + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break '=' + emitter.instruction("mov BYTE PTR [r10], 13"); // write the soft-break carriage return + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break carriage return + emitter.instruction("mov BYTE PTR [r10], 10"); // write the soft-break line feed + emitter.instruction("add r10, 1"); // advance the output cursor past the soft-break line feed + emitter.instruction("mov r11, 3"); // the moved escape occupies the first three columns of the new line + + emitter.label("__rt_qpenc_write_x86"); + emitter.instruction("mov BYTE PTR [r10], 61"); // write the escape introducer + emitter.instruction("add r10, 1"); // advance the output cursor past the escape introducer + emitter.instruction("mov edx, eax"); // copy the escaped byte before isolating its high nibble + emitter.instruction("shr edx, 4"); // isolate the high nibble of the escaped byte + emitter.instruction("cmp edx, 10"); // is the high nibble a decimal digit? + emitter.instruction("jb __rt_qpenc_hi_digit_x86"); // digits map onto '0'-'9' + emitter.instruction("add edx, 55"); // map 10-15 onto the uppercase 'A'-'F' php-src uses + emitter.instruction("jmp __rt_qpenc_hi_write_x86"); // the high nibble is ready to write + + emitter.label("__rt_qpenc_hi_digit_x86"); + emitter.instruction("add edx, 48"); // map 0-9 onto '0'-'9' + + emitter.label("__rt_qpenc_hi_write_x86"); + emitter.instruction("mov BYTE PTR [r10], dl"); // write the high hex digit + emitter.instruction("add r10, 1"); // advance the output cursor past the high hex digit + emitter.instruction("mov edx, eax"); // copy the escaped byte before isolating its low nibble + emitter.instruction("and edx, 15"); // isolate the low nibble of the escaped byte + emitter.instruction("cmp edx, 10"); // is the low nibble a decimal digit? + emitter.instruction("jb __rt_qpenc_lo_digit_x86"); // digits map onto '0'-'9' + emitter.instruction("add edx, 55"); // map 10-15 onto the uppercase 'A'-'F' php-src uses + emitter.instruction("jmp __rt_qpenc_lo_write_x86"); // the low nibble is ready to write + + emitter.label("__rt_qpenc_lo_digit_x86"); + emitter.instruction("add edx, 48"); // map 0-9 onto '0'-'9' + + emitter.label("__rt_qpenc_lo_write_x86"); + emitter.instruction("mov BYTE PTR [r10], dl"); // write the low hex digit + emitter.instruction("add r10, 1"); // advance the output cursor past the low hex digit + emitter.instruction("jmp __rt_qpenc_loop_x86"); // classify the next subject byte + + emitter.label("__rt_qpenc_done_x86"); + emitter.instruction("mov rax, r9"); // return the encoded payload pointer + emitter.instruction("mov rdx, r10"); // copy the output cursor into the length scratch register + emitter.instruction("sub rdx, r9"); // return the number of encoded bytes actually written + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the quoted-printable encoder spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the encoded string pair +} diff --git a/src/codegen_support/runtime/strings/quotemeta.rs b/src/codegen_support/runtime/strings/quotemeta.rs new file mode 100644 index 0000000000..362878d197 --- /dev/null +++ b/src/codegen_support/runtime/strings/quotemeta.rs @@ -0,0 +1,156 @@ +//! Purpose: +//! Emits the `__rt_quotemeta` runtime helper assembly for PHP's `quotemeta`: prefixes every +//! regular-expression metacharacter in a byte string with a single backslash. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The escaped set is php-src's `quotemeta` switch verbatim: `.` `\` `+` `*` `?` `[` `^` +//! `]` `$` `(` `)`. Every other byte, including NUL and high-bit bytes, is copied through. +//! - Membership is a single 64-bit bitmap test instead of an eleven-way compare chain: all +//! escaped characters live in the contiguous ASCII window `36..=94`, so `c - 36` indexes +//! `QUOTEMETA_ESCAPE_MASK` directly and any byte outside the window skips the test. +//! - The worst case is two output bytes per input byte, so `2 * len` is reserved through +//! `__rt_concat_reserve` before the first store and the ACTUAL written length is handed to +//! `__rt_concat_publish`. Over-reserving is safe; writing past the reservation is not. + +use crate::codegen_support::abi; +use crate::codegen_support::{emit::Emitter, platform::Arch}; + +/// Bitmap of the bytes `quotemeta` escapes, indexed by `byte - 36`. +/// +/// Bit `n` is set when the character `36 + n` must be prefixed with a backslash, covering +/// `$`(36) `(`(40) `)`(41) `*`(42) `+`(43) `.`(46) `?`(63) `[`(91) `\`(92) `]`(93) `^`(94). +/// The window is 59 characters wide, so the whole set fits one 64-bit register. +const QUOTEMETA_ESCAPE_MASK: i64 = 0x0780_0000_0800_04F1; + +/// First byte covered by `QUOTEMETA_ESCAPE_MASK`; bytes below it are never escaped. +const QUOTEMETA_WINDOW_START: u32 = 36; + +/// Width of the escape window in characters; `byte - 36` must stay below it to be tested. +const QUOTEMETA_WINDOW_LEN: u32 = 59; + +/// Emits the `__rt_quotemeta` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = source pointer, `x2` = source length. +/// Output: `x1` = result pointer, `x2` = result length. +/// +/// ABI (x86_64 System V): +/// Input: `rax` = source pointer, `rdx` = source length. +/// Output: `rax` = result pointer, `rdx` = result length. +/// +/// An empty input reserves and publishes zero bytes, which matches PHP's empty-string +/// result. The result is published through `__rt_concat_publish`, so it lives in the shared +/// concat scratch while it fits and in an owned heap block otherwise. +pub fn emit_quotemeta(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_quotemeta_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: quotemeta ---"); + emitter.label_global("__rt_quotemeta"); + + // -- reserve the worst-case two-bytes-per-input-byte result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save the frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the quotemeta helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("adds x0, x2, x2"); // compute the worst-case result size as 2 * source length + emitter.instruction("b.cs __rt_quotemeta_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov x9, x0"); // destination cursor + emitter.instruction("mov x10, x0"); // save the result start for the published pointer + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length + emitter.instruction("mov x11, x2"); // remaining source byte count + abi::emit_load_int_immediate(emitter, "x15", QUOTEMETA_ESCAPE_MASK); + + emitter.label("__rt_quotemeta_loop"); + emitter.instruction("cbz x11, __rt_quotemeta_done"); // finish once every source byte has been consumed + emitter.instruction("ldrb w12, [x1], #1"); // load the next source byte and advance the source cursor + emitter.instruction("sub x11, x11, #1"); // record that one source byte has been consumed + emitter.instruction(&format!("sub w13, w12, #{QUOTEMETA_WINDOW_START}")); // index the escape bitmap by shifting the byte into window space + emitter.instruction(&format!("cmp w13, #{QUOTEMETA_WINDOW_LEN}")); // is the byte outside the escapable window (unsigned, so low bytes wrap high)? + emitter.instruction("b.hs __rt_quotemeta_store"); // bytes outside the window are copied through untouched + emitter.instruction("lsr x14, x15, x13"); // move this character's escape bit into position 0 + emitter.instruction("tbz x14, #0, __rt_quotemeta_store"); // characters without an escape bit are copied through untouched + emitter.instruction("mov w13, #92"); // ASCII backslash is the escape prefix + emitter.instruction("strb w13, [x9], #1"); // write the escape prefix ahead of the metacharacter + + emitter.label("__rt_quotemeta_store"); + emitter.instruction("strb w12, [x9], #1"); // copy the source byte itself into the result + emitter.instruction("b __rt_quotemeta_loop"); // continue with the next source byte + + emitter.label("__rt_quotemeta_done"); + emitter.instruction("mov x1, x10"); // return the escaped string start pointer + emitter.instruction("sub x2, x9, x10"); // the written byte count is the result length + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the quotemeta helper frame + emitter.instruction("ret"); // return the escaped string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_quotemeta_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe +} + +/// Emits `__rt_quotemeta` for x86_64 Linux using the System V ABI. +/// +/// Uses `bt` against the escape bitmap so the membership test needs no `cl` shift count, +/// which keeps `rcx` free as the source countdown register. +fn emit_quotemeta_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: quotemeta ---"); + emitter.label_global("__rt_quotemeta"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the source byte count across the reservation call + emitter.instruction("mov rax, rdx"); // seed the result size from the source byte count + emitter.instruction("add rax, rax"); // compute the worst-case result size as 2 * source length + emitter.instruction("jc __rt_quotemeta_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the escaped result + emitter.instruction("mov r9, rax"); // destination cursor + emitter.instruction("mov r10, rax"); // save the result start for the published pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload the borrowed source pointer as a read cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the source byte count as a decrementing counter + emitter.instruction(&format!("mov r11, 0x{QUOTEMETA_ESCAPE_MASK:x}")); // materialize the escape bitmap for the membership test + + emitter.label("__rt_quotemeta_loop_linux_x86_64"); + emitter.instruction("test rcx, rcx"); // stop once every source byte has been consumed + emitter.instruction("je __rt_quotemeta_done_linux_x86_64"); // finish when the source string has been fully escaped + emitter.instruction("movzx eax, BYTE PTR [rsi]"); // load the next source byte and widen it for the window test + emitter.instruction("add rsi, 1"); // advance the source cursor after consuming one byte + emitter.instruction("sub rcx, 1"); // record that one source byte has been consumed + emitter.instruction("mov edx, eax"); // copy the source byte before shifting it into window space + emitter.instruction(&format!("sub edx, {QUOTEMETA_WINDOW_START}")); // index the escape bitmap by shifting the byte into window space + emitter.instruction(&format!("cmp edx, {QUOTEMETA_WINDOW_LEN}")); // is the byte outside the escapable window (unsigned, so low bytes wrap high)? + emitter.instruction("jae __rt_quotemeta_store_linux_x86_64"); // bytes outside the window are copied through untouched + emitter.instruction("bt r11, rdx"); // test this character's escape bit inside the bitmap + emitter.instruction("jnc __rt_quotemeta_store_linux_x86_64"); // characters without an escape bit are copied through untouched + emitter.instruction("mov BYTE PTR [r9], 92"); // write the ASCII backslash escape prefix + emitter.instruction("add r9, 1"); // advance the destination cursor past the escape prefix + + emitter.label("__rt_quotemeta_store_linux_x86_64"); + emitter.instruction("mov BYTE PTR [r9], al"); // copy the source byte itself into the result + emitter.instruction("add r9, 1"); // advance the destination cursor past the copied byte + emitter.instruction("jmp __rt_quotemeta_loop_linux_x86_64"); // continue with the next source byte + + emitter.label("__rt_quotemeta_done_linux_x86_64"); + emitter.instruction("mov rax, r10"); // return the escaped string start pointer + emitter.instruction("mov rdx, r9"); // copy the destination cursor into the length scratch register + emitter.instruction("sub rdx, r10"); // the written byte count is the result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the quotemeta spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the escaped string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_quotemeta_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller +} diff --git a/src/codegen_support/runtime/strings/rawurlencode.rs b/src/codegen_support/runtime/strings/rawurlencode.rs index 2f7143f5ab..ec9a106bf1 100644 --- a/src/codegen_support/runtime/strings/rawurlencode.rs +++ b/src/codegen_support/runtime/strings/rawurlencode.rs @@ -7,21 +7,26 @@ //! //! Key details: //! - URL encoding helpers are emitted byte scanners that must preserve PHP escaping rules for supported encodings. +//! - The worst-case `3 * len` percent-encoded result is reserved through `__rt_concat_reserve` +//! before the first store, so long inputs fall back to heap storage instead of running off +//! the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `__rt_rawurlencode` runtime helper for rawurlencode (RFC 3986). /// /// Percent-encodes all bytes except alphanumeric and `-_.~`. Each unsafe byte -/// expands to three bytes (`%XX` where XX is uppercase hex). The result is appended -/// to the concatenation buffer. +/// expands to three bytes (`%XX` where XX is uppercase hex). The worst-case `3 * len` +/// expansion is reserved through `__rt_concat_reserve` (concat scratch while it fits, +/// owned heap storage otherwise) and finished through `__rt_concat_publish`. /// /// ABI (ARM64): /// - Input: x1=source pointer, x2=source length /// - Output: x1=result pointer, x2=result length -/// - Clobbers: x6-x13, concat buffer offset is updated +/// - Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// - A wrapped `3 * len` product reports PHP's allocation-overflow fatal through +/// `__rt_alloc_overflow` instead of reserving a too-small destination. pub fn emit_rawurlencode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_rawurlencode_linux_x86_64(emitter); @@ -32,12 +37,19 @@ pub fn emit_rawurlencode(emitter: &mut Emitter) { emitter.comment("--- runtime: rawurlencode ---"); emitter.label_global("__rt_rawurlencode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case three-bytes-per-input-byte result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the rawurlencode helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x9, #3"); // worst-case percent-encoded expansion factor + emitter.instruction("umulh x10, x2, x9"); // capture the high half of the 3 * length product + emitter.instruction("cbnz x10, __rt_rawurlencode_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x0, x2, x9"); // compute the worst-case percent-encoded result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the percent-encoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_rawurlencode_loop"); @@ -105,10 +117,14 @@ pub fn emit_rawurlencode(emitter: &mut Emitter) { emitter.label("__rt_rawurlencode_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the rawurlencode helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_rawurlencode_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of the `__rt_rawurlencode` runtime helper. @@ -119,19 +135,24 @@ pub fn emit_rawurlencode(emitter: &mut Emitter) { /// ABI (x86_64 System V): /// - Input: rax=source pointer, rdx=source length /// - Output: rax=result pointer, rdx=result length -/// - Clobbers: rcx, rsi, rdx, r8-r11, concat buffer offset is updated +/// - Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. fn emit_rawurlencode_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: rawurlencode ---"); emitter.label_global("__rt_rawurlencode"); - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before RFC 3986 percent-encoding the borrowed source string - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the rawurlencoded string begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("imul rax, rdx, 3"); // compute the worst-case percent-encoded result size as 3 * source length + emitter.instruction("jo __rt_rawurlencode_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the percent-encoded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the rawurlencoded string begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_rawurlencode_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied or percent-encoded into concat storage @@ -198,11 +219,15 @@ fn emit_rawurlencode_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_rawurlencode_loop_linux_x86_64"); // continue encoding the remaining source bytes after copying one safe byte emitter.label("__rt_rawurlencode_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after percent-encoding the full input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the encoded string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after percent-encoding the full input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the encoded string length emitter.instruction("sub rdx, r8"); // compute the encoded string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that rawurlencode() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced encoded-string length - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the rawurlencode() pass - emitter.instruction("ret"); // return the concat-backed rawurlencoded string in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the rawurlencode spill slots before returning the encoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the encoded string + emitter.instruction("ret"); // return the rawurlencoded string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_rawurlencode_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/sprintf.rs b/src/codegen_support/runtime/strings/sprintf.rs index 77a2d8c0ff..9eca3ef46f 100644 --- a/src/codegen_support/runtime/strings/sprintf.rs +++ b/src/codegen_support/runtime/strings/sprintf.rs @@ -1,30 +1,73 @@ //! Purpose: -//! Emits the `__rt_sprintf`, `__rt_sprintf_loop` runtime helper assembly for sprintf formatting. -//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! Emits the `__rt_sprintf` runtime helper assembly, the shared PHP `printf`-family +//! formatter behind `sprintf()`, `printf()`, `fprintf()`, and (through `__rt_vsprintf`) +//! the `v*printf()` family. This file owns the AArch64 lowering; the Linux x86_64 +//! lowering lives in `sprintf_x86_64.rs` and must stay behaviourally identical. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: -//! - Formatting helpers parse format strings and marshal values through target ABI calls or emitted formatting paths. - +//! - The helper parses each `%` specifier itself into numeric registers/frame slots +//! (argument number, flags, pad character, width, precision, conversion character). +//! Format bytes supplied by the program are never copied verbatim into the C format +//! string handed to libc, so an over-long specifier cannot overrun the mini format +//! buffer and an unknown conversion (notably `%n`) never reaches `snprintf`. +//! - Width and padding are applied by this helper, not by `snprintf`. libc only renders +//! the unpadded numeric body into a fixed 512-byte scratch, whose worst case +//! (`%.53f` of `DBL_MAX` → 363 bytes) is bounded because precision is clamped to +//! PHP's 53-digit maximum. `%s`, `%b`, and `%c` bypass libc entirely. +//! - Every byte written into `_concat_buf` is bounds-checked against the end of that +//! 64 KiB arena; an oversized result is a controlled fatal, never an overrun. +//! - The 16-byte argument records carry a type tag, and each conversion coerces the operand +//! to what it needs (double↔int, and string→number through `__rt_str_to_int` / +//! `__rt_str_to_number`). A record whose tag disagrees with the conversion character — +//! which happens for `v*printf()`, a runtime-built format string, or `%1$s`/`%1$d` on one +//! argument — is therefore converted, never printed as a raw pointer. +//! - PHP's `%e`/`%E` exponent is not zero-padded (`1.234568e+4`, not `e+04`), so the +//! libc output is compacted in place before it is emitted. + +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::{Arch, Platform}; +use crate::codegen_support::runtime::data::{ + SPRINTF_ARGCOUNT_MSG, SPRINTF_OVERFLOW_MSG, SPRINTF_UNKNOWN_SPEC_MSG, SPRINTF_WIDTH_MSG, +}; use super::sprintf_x86_64::emit_sprintf_linux_x86_64; -/// Emits the `__rt_sprintf` global runtime helper for sprintf-style formatting. -/// Uses x0=arg_count, x1=fmt_ptr, x2=fmt_len on entry; args pushed on stack (16 bytes each). -/// Returns x1=result_ptr, x2=result_len in concat_buf. Updates `_concat_off` atomically. +/// Byte capacity of the shared `_concat_buf` result arena declared in +/// `crate::codegen_support::runtime::data::emit_runtime_data_fixed`. Both `__rt_sprintf` +/// lowerings derive their write limit from this constant, so the two stay in step. +pub(super) const CONCAT_BUF_CAP: u32 = 65536; + +/// Byte capacity of the per-conversion `snprintf` scratch buffer. PHP clamps float +/// precision to 53 digits, so the widest libc body is `%.53f` of `DBL_MAX` +/// (309 integer digits + `.` + 53 fraction digits + sign = 364 bytes); 512 leaves +/// headroom and every copy out of it is still clamped to `CONV_SCRATCH_CAP - 1`. +pub(super) const CONV_SCRATCH_CAP: u32 = 512; + +/// Emits the `__rt_sprintf` global runtime helper for `printf`-family formatting. +/// +/// # Input (AArch64) +/// - `x0`: number of packed variadic argument records pushed by the caller +/// - `x1`: format string pointer +/// - `x2`: format string byte length +/// - `[sp]` of the caller: `x0` records of 16 bytes, `[payload, tag]`, first argument lowest /// -/// Each stack argument is [value, type_tag] where type_tag: 0=int, 1=str(len<<8), 2=float, 3=bool. -/// The runtime pops arg_count*16 bytes from the caller's stack before returning. +/// # Output (AArch64) +/// - `x1`: result pointer inside `_concat_buf` +/// - `x2`: result byte length /// -/// Callee-saved registers used: x19=fmt_ptr, x20=fmt_remaining_len, x21=arg_index, -/// x22=args_base, x23=dest_ptr, x24=result_start, x25=concat_off_ptr, x26=arg_count. +/// The record tag word is `0` for int, `1 | (len << 8)` for string, `2` for float and +/// `3` for bool; the helper consults it so a conversion never dereferences a payload that +/// is not a string pointer. `_concat_off` is advanced by the result length and the caller's +/// `arg_count * 16` bytes of records are popped before returning. /// -/// Delegates format specifier processing (flags, width, precision, type char) to libc snprintf -/// for correct handling. On Apple ARM64, variadic arguments for snprintf are passed at [sp]. +/// Callee-saved registers used: `x19` = format cursor, `x20` = remaining format bytes, +/// `x21` = next sequential argument index, `x22` = argument record base, `x23` = write +/// cursor in `_concat_buf`, `x24` = result start, `x25` = `_concat_off` address, +/// `x26` = argument count. pub fn emit_sprintf(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_sprintf_linux_x86_64(emitter); @@ -35,343 +78,793 @@ pub fn emit_sprintf(emitter: &mut Emitter) { emitter.comment("--- runtime: sprintf ---"); emitter.label_global("__rt_sprintf"); - // Frame layout (288 bytes): - // sp+0..7 = variadic arg slot for snprintf (must be at sp) - // sp+8..15 = (padding for 16-byte alignment of variadic) - // sp+16..23 = saved x19 - // sp+24..31 = saved x20 - // sp+32..39 = saved x21 - // sp+40..47 = saved x22 - // sp+48..55 = saved x23 - // sp+56..63 = saved x24 - // sp+64..71 = saved x25 - // sp+72..79 = saved x26 - // sp+80..111 = mini format string buffer (32 bytes) - // sp+112..239 = snprintf output buffer (128 bytes) - // sp+240..367 = string null-term copy buffer (128 bytes) - // sp+368..375 = saved x29 - // sp+376..383 = saved x30 - // - // Callee-saved register usage: - // x19 = fmt_ptr (current position in format string) - // x20 = fmt_remaining_len - // x21 = arg_index - // x22 = args_base pointer (points to pushed args from caller) - // x23 = dest pointer (current write position in concat_buf) - // x24 = result_start pointer (beginning of result in concat_buf) - // x25 = concat_off pointer - // x26 = arg_count - - emitter.instruction("sub sp, sp, #384"); // allocate stack frame - emitter.instruction("stp x29, x30, [sp, #368]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #368"); // set frame pointer + // Frame layout (704 bytes). Every stp/ldp offset stays inside the ±504 scaled + // immediate range, so the saved register pairs live at the bottom of the frame: + // sp+0..7 = first variadic slot for snprintf (Apple AArch64 needs it at sp) + // sp+8..15 = padding that keeps the variadic slot 16-byte aligned + // sp+16..31 = saved x29, x30 + // sp+32..95 = saved x19..x26 + // sp+96..103 = parsed field width + // sp+104..111 = parsed precision (-1 when the specifier had no '.') + // sp+112..119 = parsed flags: bit0 left-align, bit1 force sign, bit2 alternate form + // sp+120..127 = parsed pad character + // sp+128..135 = parsed conversion character + // sp+136..143 = parsed argument number (0 = consume the next sequential argument) + // sp+144..151 = one-past-the-end address of _concat_buf + // sp+152..159 = padding + // sp+160..191 = mini C format string built by this helper (never copied from input) + // sp+192..703 = snprintf conversion scratch (CONV_SCRATCH_CAP bytes) + + emitter.instruction("sub sp, sp, #704"); // allocate the sprintf helper frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // set frame pointer // -- save callee-saved registers -- - emitter.instruction("stp x19, x20, [sp, #16]"); // save x19, x20 - emitter.instruction("stp x21, x22, [sp, #32]"); // save x21, x22 - emitter.instruction("stp x23, x24, [sp, #48]"); // save x23, x24 - emitter.instruction("stp x25, x26, [sp, #64]"); // save x25, x26 + emitter.instruction("stp x19, x20, [sp, #32]"); // save x19, x20 + emitter.instruction("stp x21, x22, [sp, #48]"); // save x21, x22 + emitter.instruction("stp x23, x24, [sp, #64]"); // save x23, x24 + emitter.instruction("stp x25, x26, [sp, #80]"); // save x25, x26 // -- initialize state in callee-saved registers -- - emitter.instruction("mov x19, x1"); // fmt_ptr - emitter.instruction("mov x20, x2"); // fmt_remaining_len - emitter.instruction("mov x26, x0"); // arg_count - emitter.instruction("mov x21, #0"); // arg_index = 0 - emitter.instruction("add x22, sp, #384"); // args_base (past our frame) - - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x25", "_concat_off"); - emitter.instruction("ldr x8, [x25]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x23, x7, x8"); // dest pointer = buf + offset - emitter.instruction("mov x24, x23"); // save result start - - // -- main format scanning loop -- + emitter.instruction("mov x19, x1"); // format cursor + emitter.instruction("mov x20, x2"); // remaining format bytes + emitter.instruction("mov x26, x0"); // packed argument record count + emitter.instruction("mov x21, #0"); // next sequential argument index + emitter.instruction("add x22, sp, #704"); // argument record base (just past this frame) + + // -- set up the concat_buf destination and its hard write limit -- + abi::emit_symbol_address(emitter, "x25", "_concat_off"); + emitter.instruction("ldr x8, [x25]"); // current concat-buffer write offset + abi::emit_symbol_address(emitter, "x7", "_concat_buf"); + emitter.instruction("add x23, x7, x8"); // write cursor = buffer base + offset + emitter.instruction("mov x24, x23"); // remember where this result starts + emitter.instruction(&format!("mov x9, #{}", CONCAT_BUF_CAP)); // total concat-buffer capacity in bytes + emitter.instruction("add x9, x7, x9"); // one-past-the-end address of the concat buffer + emitter.instruction("str x9, [sp, #144]"); // publish the hard write limit for every copy below + + // ================================================================ + // MAIN SCAN LOOP: literal bytes are copied, '%' starts a specifier + // ================================================================ emitter.label("__rt_sprintf_loop"); - emitter.instruction("cbz x20, __rt_sprintf_done"); // no format chars left - emitter.instruction("ldrb w12, [x19], #1"); // load format char, advance - emitter.instruction("sub x20, x20, #1"); // decrement remaining + emitter.instruction("cbz x20, __rt_sprintf_done"); // no format bytes left + emitter.instruction("ldrb w12, [x19], #1"); // load the next format byte and advance + emitter.instruction("sub x20, x20, #1"); // account for the consumed format byte emitter.instruction("cmp w12, #37"); // is it '%'? - emitter.instruction("b.eq __rt_sprintf_fmt"); // yes → process format specifier - - // -- literal char: copy to output -- - emitter.instruction("strb w12, [x23], #1"); // copy literal char to output - emitter.instruction("b __rt_sprintf_loop"); // next char + emitter.instruction("b.eq __rt_sprintf_fmt"); // yes → parse a conversion specifier + emitter.instruction("ldr x9, [sp, #144]"); // reload the concat-buffer write limit + emitter.instruction("cmp x23, x9"); // would this literal byte land outside the arena? + emitter.instruction("b.hs __rt_sprintf_ofatal"); // yes → controlled fatal instead of an overrun + emitter.instruction("strb w12, [x23], #1"); // copy the literal byte to the result + emitter.instruction("b __rt_sprintf_loop"); // continue scanning - // -- process format specifier -- emitter.label("__rt_sprintf_fmt"); - emitter.instruction("cbz x20, __rt_sprintf_done"); // no char after % → done - emitter.instruction("ldrb w12, [x19]"); // peek at next char - - // -- %% → literal % -- - emitter.instruction("cmp w12, #37"); // is it '%'? - emitter.instruction("b.ne __rt_sprintf_scan_spec"); // no → scan full specifier + emitter.instruction("cbz x20, __rt_sprintf_done"); // trailing '%' with nothing after it + emitter.instruction("ldrb w12, [x19]"); // peek at the byte after '%' + emitter.instruction("cmp w12, #37"); // is the sequence '%%'? + emitter.instruction("b.ne __rt_sprintf_spec"); // no → parse a real specifier emitter.instruction("add x19, x19, #1"); // consume the second '%' - emitter.instruction("sub x20, x20, #1"); // decrement remaining - emitter.instruction("strb w12, [x23], #1"); // write literal '%' to output - emitter.instruction("b __rt_sprintf_loop"); // next - - // -- scan format specifier into mini buffer at sp+80 -- - // Build: '%' + [flags] + [width] + [.precision] + [ll] + type_char + '\0' - emitter.label("__rt_sprintf_scan_spec"); - emitter.instruction("add x10, sp, #80"); // mini format buffer start - emitter.instruction("mov w15, #37"); // '%' character - emitter.instruction("strb w15, [x10], #1"); // write '%' to mini buffer - - // -- scan flags: '-', '+', '0', ' ', '#' -- - emitter.label("__rt_sprintf_scan_flags"); - emitter.instruction("cbz x20, __rt_sprintf_end_spec"); // no chars left - emitter.instruction("ldrb w12, [x19]"); // peek at current char - emitter.instruction("cmp w12, #45"); // '-' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it - emitter.instruction("cmp w12, #43"); // '+' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it - emitter.instruction("cmp w12, #48"); // '0' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it - emitter.instruction("cmp w12, #32"); // ' ' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it - emitter.instruction("cmp w12, #35"); // '#' flag? - emitter.instruction("b.eq __rt_sprintf_copy_flag"); // yes → copy it - emitter.instruction("b __rt_sprintf_scan_width"); // no flag → try width - - emitter.label("__rt_sprintf_copy_flag"); - emitter.instruction("strb w12, [x10], #1"); // copy flag char to mini buffer - emitter.instruction("add x19, x19, #1"); // consume char from format - emitter.instruction("sub x20, x20, #1"); // decrement remaining - emitter.instruction("b __rt_sprintf_scan_flags"); // check for more flags - - // -- scan width: digits -- - emitter.label("__rt_sprintf_scan_width"); - emitter.instruction("cbz x20, __rt_sprintf_end_spec"); // no chars left - emitter.instruction("ldrb w12, [x19]"); // peek at current char - emitter.instruction("cmp w12, #48"); // < '0'? - emitter.instruction("b.lt __rt_sprintf_scan_dot"); // yes → try precision dot - emitter.instruction("cmp w12, #57"); // > '9'? - emitter.instruction("b.gt __rt_sprintf_scan_dot"); // yes → try precision dot - emitter.instruction("strb w12, [x10], #1"); // copy width digit to mini buffer - emitter.instruction("add x19, x19, #1"); // consume char - emitter.instruction("sub x20, x20, #1"); // decrement remaining - emitter.instruction("b __rt_sprintf_scan_width"); // check for more digits - - // -- scan precision: '.' followed by digits -- - emitter.label("__rt_sprintf_scan_dot"); - emitter.instruction("cmp w12, #46"); // '.' ? - emitter.instruction("b.ne __rt_sprintf_scan_type"); // no → must be type char - emitter.instruction("strb w12, [x10], #1"); // copy '.' to mini buffer - emitter.instruction("add x19, x19, #1"); // consume '.' - emitter.instruction("sub x20, x20, #1"); // decrement remaining - - emitter.label("__rt_sprintf_scan_prec"); - emitter.instruction("cbz x20, __rt_sprintf_end_spec"); // no chars left - emitter.instruction("ldrb w12, [x19]"); // peek at current char - emitter.instruction("cmp w12, #48"); // < '0'? - emitter.instruction("b.lt __rt_sprintf_scan_type"); // no → type char - emitter.instruction("cmp w12, #57"); // > '9'? - emitter.instruction("b.gt __rt_sprintf_scan_type"); // no → type char - emitter.instruction("strb w12, [x10], #1"); // copy precision digit - emitter.instruction("add x19, x19, #1"); // consume char - emitter.instruction("sub x20, x20, #1"); // decrement remaining - emitter.instruction("b __rt_sprintf_scan_prec"); // check for more digits - - // -- read type character -- - emitter.label("__rt_sprintf_scan_type"); - emitter.instruction("cbz x20, __rt_sprintf_end_spec"); // no chars left - emitter.instruction("ldrb w12, [x19], #1"); // load type char, consume it - emitter.instruction("sub x20, x20, #1"); // decrement remaining - - // Dispatch by type character - emitter.instruction("cmp w12, #102"); // 'f' ? - emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float - emitter.instruction("cmp w12, #101"); // 'e' ? - emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float - emitter.instruction("cmp w12, #103"); // 'g' ? - emitter.instruction("b.eq __rt_sprintf_type_float"); // yes → float - emitter.instruction("cmp w12, #115"); // 's' ? - emitter.instruction("b.eq __rt_sprintf_type_str"); // yes → string - emitter.instruction("b __rt_sprintf_type_int"); // default → integer - - // -- incomplete specifier at end of format string -- - emitter.label("__rt_sprintf_end_spec"); - emitter.instruction("b __rt_sprintf_done"); // bail out + emitter.instruction("sub x20, x20, #1"); // account for the consumed byte + emitter.instruction("ldr x9, [sp, #144]"); // reload the concat-buffer write limit + emitter.instruction("cmp x23, x9"); // would the literal '%' land outside the arena? + emitter.instruction("b.hs __rt_sprintf_ofatal"); // yes → controlled fatal instead of an overrun + emitter.instruction("strb w12, [x23], #1"); // emit the literal '%' + emitter.instruction("b __rt_sprintf_loop"); // continue scanning + + emit_spec_parser(emitter); + emit_argument_fetch(emitter); + emit_conversion_dispatch(emitter); + emit_string_conversion(emitter); + emit_binary_conversion(emitter); + emit_char_conversion(emitter); + emit_integer_conversion(emitter); + emit_float_conversion(emitter); + emit_snprintf_result(emitter); + emit_exponent_compaction(emitter); + emit_pad_and_copy(emitter); // ================================================================ - // FLOAT: %f, %e, %g (with optional flags/width/precision) - // Passes the double value on the stack at [sp] for variadic ABI. + // DONE: publish the result and pop the caller's argument records // ================================================================ - emitter.label("__rt_sprintf_type_float"); - emitter.instruction("strb w12, [x10], #1"); // copy type char to mini buffer - emitter.instruction("strb wzr, [x10]"); // null-terminate format string + emitter.label("__rt_sprintf_done"); + emitter.instruction("mov x1, x24"); // result pointer inside the concat buffer + emitter.instruction("sub x2, x23, x24"); // result byte length + + // -- update concat_off -- + emitter.instruction("ldr x8, [x25]"); // current concat-buffer write offset + emitter.instruction("add x8, x8, x2"); // advance it past this result + emitter.instruction("str x8, [x25]"); // publish the new write offset + + // -- prepare to pop the caller's packed argument records -- + emitter.instruction("mov x0, x26"); // packed argument record count + emitter.instruction("lsl x0, x0, #4"); // records are 16 bytes each + + // -- restore callee-saved registers and unwind -- + emitter.instruction("ldp x19, x20, [sp, #32]"); // restore x19, x20 + emitter.instruction("ldp x21, x22, [sp, #48]"); // restore x21, x22 + emitter.instruction("ldp x23, x24, [sp, #64]"); // restore x23, x24 + emitter.instruction("ldp x25, x26, [sp, #80]"); // restore x25, x26 + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #704"); // release the sprintf helper frame + emitter.instruction("add sp, sp, x0"); // pop the caller's packed argument records + emitter.instruction("ret"); // return the formatted string in x1/x2 + + emit_fatal_paths(emitter); +} + +/// Emits an AArch64 decimal-number scanner used for the argument number, the field width, +/// and the precision. +/// +/// `ptr`/`len` are the source cursor and remaining-byte count; both are advanced past the +/// digits that were consumed. `acc` receives the parsed value and `count` the digit count; +/// both must be zeroed by the caller. `w15` is left holding the first non-digit byte, or +/// zero when the input ran out (so the caller can distinguish "stopped on `$`" from +/// "ran out of format"). `w12`/`x12` are clobbered. +/// +/// Accumulation stops after 10 digits and any longer run saturates to `0x80000000`, which +/// keeps the accumulator inside 64 bits and makes "wider than `INT_MAX`" detectable as +/// `acc >> 31 != 0` no matter how many digits the program supplied. +fn emit_scan_decimal(emitter: &mut Emitter, prefix: &str, ptr: &str, len: &str, acc: &str, count: &str) { + emitter.label(&format!("{}_loop", prefix)); + emitter.instruction(&format!("cbz {}, {}_end0", len, prefix)); // ran out of format bytes + emitter.instruction(&format!("ldrb w15, [{}]", ptr)); // peek at the current byte + emitter.instruction("sub w12, w15, #48"); // convert the byte to a digit value + emitter.instruction("cmp w12, #9"); // is it outside '0'..'9'? + emitter.instruction(&format!("b.hi {}_done", prefix)); // yes → the number ends here + emitter.instruction(&format!("cmp {}, #10", count)); // already accumulated ten digits? + emitter.instruction(&format!("b.hs {}_skip", prefix)); // yes → stop accumulating, just count + emitter.instruction(&format!("add {0}, {0}, {0}, lsl #2", acc)); // accumulator *= 5 + emitter.instruction(&format!("lsl {0}, {0}, #1", acc)); // accumulator *= 2, so *= 10 overall + emitter.instruction(&format!("add {0}, {0}, x12", acc)); // add the current digit + emitter.label(&format!("{}_skip", prefix)); + emitter.instruction(&format!("add {0}, {0}, #1", count)); // count the consumed digit + emitter.instruction(&format!("add {0}, {0}, #1", ptr)); // advance the source cursor + emitter.instruction(&format!("sub {0}, {0}, #1", len)); // account for the consumed byte + emitter.instruction(&format!("b {}_loop", prefix)); // scan the next digit + emitter.label(&format!("{}_end0", prefix)); + emitter.instruction("mov w15, #0"); // no lookahead byte is available + emitter.label(&format!("{}_done", prefix)); + emitter.instruction(&format!("cmp {}, #10", count)); // did the run exceed ten digits? + emitter.instruction(&format!("b.ls {}_nosat", prefix)); // no → keep the accumulated value + emitter.instruction(&format!("mov {}, #0x80000000", acc)); // saturate above INT_MAX so the range check fires + emitter.label(&format!("{}_nosat", prefix)); +} + +/// Emits the AArch64 specifier parser: argument number, flags, pad character, width and +/// precision are decoded into frame slots and the conversion character is stored last. +/// +/// Nothing here copies program-supplied bytes into a buffer, so an arbitrarily long +/// specifier costs scan time only — it can never overrun the mini format buffer. +fn emit_spec_parser(emitter: &mut Emitter) { + // -- reset the per-specifier state -- + emitter.label("__rt_sprintf_spec"); + emitter.instruction("str xzr, [sp, #96]"); // width = 0 + emitter.instruction("mov x9, #-1"); // sentinel meaning "no precision given" + emitter.instruction("str x9, [sp, #104]"); // precision = absent + emitter.instruction("str xzr, [sp, #112]"); // flags = none + emitter.instruction("mov w9, #32"); // PHP's default pad character is a space + emitter.instruction("str x9, [sp, #120]"); // pad character = ' ' + emitter.instruction("str xzr, [sp, #136]"); // argument number = sequential + + // -- optional "N$" argument number: only committed when a '$' follows the digits -- + emitter.instruction("mov x9, x19"); // lookahead cursor (does not consume yet) + emitter.instruction("mov x10, x20"); // lookahead remaining-byte count + emitter.instruction("mov x11, #0"); // argument-number accumulator + emitter.instruction("mov x14, #0"); // argument-number digit count + emit_scan_decimal(emitter, "__rt_sprintf_an", "x9", "x10", "x11", "x14"); + emitter.instruction("cbz x14, __rt_sprintf_flags"); // no digits → not an argument number + emitter.instruction("cmp w15, #36"); // is the byte after the digits '$'? + emitter.instruction("b.ne __rt_sprintf_flags"); // no → those digits are the field width + emitter.instruction("str x11, [sp, #136]"); // commit the explicit argument number + emitter.instruction("add x19, x9, #1"); // consume the digits and the '$' + emitter.instruction("sub x20, x10, #1"); // account for the consumed '$' + + // -- flags: '-', '+', '0', ' ', '#', and PHP's "'X" custom pad character -- + emitter.label("__rt_sprintf_flags"); + emitter.instruction("cbz x20, __rt_sprintf_endspec"); // format ended inside the specifier + emitter.instruction("ldrb w12, [x19]"); // peek at the current specifier byte + emitter.instruction("cmp w12, #45"); // '-' left-align flag? + emitter.instruction("b.eq __rt_sprintf_fl_left"); // yes → record left alignment + emitter.instruction("cmp w12, #43"); // '+' force-sign flag? + emitter.instruction("b.eq __rt_sprintf_fl_plus"); // yes → record the forced sign + emitter.instruction("cmp w12, #48"); // '0' zero-pad flag? + emitter.instruction("b.eq __rt_sprintf_fl_zero"); // yes → pad character becomes '0' + emitter.instruction("cmp w12, #32"); // ' ' space-pad flag? + emitter.instruction("b.eq __rt_sprintf_fl_space"); // yes → pad character becomes ' ' + emitter.instruction("cmp w12, #35"); // '#' alternate-form flag? + emitter.instruction("b.eq __rt_sprintf_fl_alt"); // yes → record the alternate form + emitter.instruction("cmp w12, #39"); // "'" custom-pad-character flag? + emitter.instruction("b.eq __rt_sprintf_fl_pad"); // yes → the next byte is the pad character + emitter.instruction("b __rt_sprintf_width"); // no more flags → parse the width + + emitter.label("__rt_sprintf_fl_left"); + emitter.instruction("ldr x9, [sp, #112]"); // load the parsed flags + emitter.instruction("orr x9, x9, #1"); // set the left-align bit + emitter.instruction("str x9, [sp, #112]"); // store the parsed flags + emitter.instruction("b __rt_sprintf_fl_next"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_plus"); + emitter.instruction("ldr x9, [sp, #112]"); // load the parsed flags + emitter.instruction("orr x9, x9, #2"); // set the force-sign bit + emitter.instruction("str x9, [sp, #112]"); // store the parsed flags + emitter.instruction("b __rt_sprintf_fl_next"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_alt"); + emitter.instruction("ldr x9, [sp, #112]"); // load the parsed flags + emitter.instruction("orr x9, x9, #4"); // set the alternate-form bit + emitter.instruction("str x9, [sp, #112]"); // store the parsed flags + emitter.instruction("b __rt_sprintf_fl_next"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_zero"); + emitter.instruction("mov w9, #48"); // '0' becomes the pad character + emitter.instruction("str x9, [sp, #120]"); // store the pad character + emitter.instruction("b __rt_sprintf_fl_next"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_space"); + emitter.instruction("mov w9, #32"); // ' ' becomes the pad character + emitter.instruction("str x9, [sp, #120]"); // store the pad character + emitter.instruction("b __rt_sprintf_fl_next"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_pad"); + emitter.instruction("add x19, x19, #1"); // consume the "'" introducer + emitter.instruction("sub x20, x20, #1"); // account for the consumed byte + emitter.instruction("cbz x20, __rt_sprintf_endspec"); // "'" at end of format → nothing to pad with + emitter.instruction("ldrb w9, [x19]"); // the next byte is the custom pad character + emitter.instruction("str x9, [sp, #120]"); // store the custom pad character + + emitter.label("__rt_sprintf_fl_next"); + emitter.instruction("add x19, x19, #1"); // consume the flag byte + emitter.instruction("sub x20, x20, #1"); // account for the consumed byte + emitter.instruction("b __rt_sprintf_flags"); // look for another flag + + // -- field width -- + emitter.label("__rt_sprintf_width"); + emitter.instruction("mov x11, #0"); // width accumulator + emitter.instruction("mov x14, #0"); // width digit count + emit_scan_decimal(emitter, "__rt_sprintf_w", "x19", "x20", "x11", "x14"); + emitter.instruction("str x11, [sp, #96]"); // store the parsed field width + + // -- optional ".precision" -- + emitter.instruction("cbz x20, __rt_sprintf_endspec"); // format ended before the conversion + emitter.instruction("ldrb w12, [x19]"); // peek at the current specifier byte + emitter.instruction("cmp w12, #46"); // '.' precision introducer? + emitter.instruction("b.ne __rt_sprintf_stype"); // no → the conversion character follows + emitter.instruction("add x19, x19, #1"); // consume the '.' + emitter.instruction("sub x20, x20, #1"); // account for the consumed byte + emitter.instruction("mov x11, #0"); // precision accumulator ('.' alone means 0) + emitter.instruction("mov x14, #0"); // precision digit count + emit_scan_decimal(emitter, "__rt_sprintf_p", "x19", "x20", "x11", "x14"); + emitter.instruction("str x11, [sp, #104]"); // store the parsed precision + + // -- conversion character -- + emitter.label("__rt_sprintf_stype"); + emitter.instruction("cbz x20, __rt_sprintf_endspec"); // format ended before the conversion + emitter.instruction("ldrb w12, [x19], #1"); // load the conversion character and advance + emitter.instruction("sub x20, x20, #1"); // account for the consumed byte + emitter.instruction("str x12, [sp, #128]"); // store the conversion character + emitter.instruction("b __rt_sprintf_arg"); // fetch the argument this conversion consumes + + emitter.label("__rt_sprintf_endspec"); + emitter.instruction("b __rt_sprintf_done"); // truncated specifier → stop formatting +} + +/// Emits the AArch64 argument fetch: resolves the sequential or explicit `N$` argument +/// index, rejects out-of-range indices, and loads the 16-byte record into `x3`/`x4`. +/// +/// The range check is what keeps the helper from reading the caller's stack past the +/// pushed records when a format string requests more arguments than were supplied. +fn emit_argument_fetch(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_arg"); + emitter.instruction("ldr x13, [sp, #136]"); // parsed argument number (0 = sequential) + emitter.instruction("cbz x13, __rt_sprintf_arg_seq"); // no explicit number → take the next argument + emitter.instruction("sub x9, x13, #1"); // PHP argument numbers are 1-based + emitter.instruction("b __rt_sprintf_arg_have"); // index resolved + emitter.label("__rt_sprintf_arg_seq"); + emitter.instruction("mov x9, x21"); // consume the next sequential argument + emitter.instruction("add x21, x21, #1"); // advance the sequential cursor + emitter.label("__rt_sprintf_arg_have"); + emitter.instruction("cmp x9, x26"); // is the index within the supplied records? + emitter.instruction("b.hs __rt_sprintf_afatal"); // no → controlled fatal instead of a stack read + emitter.instruction("lsl x10, x9, #4"); // records are 16 bytes each + emitter.instruction("add x10, x22, x10"); // address of the selected record + emitter.instruction("ldr x3, [x10]"); // record payload word + emitter.instruction("ldr x4, [x10, #8]"); // record tag word (tag | length << 8) + emitter.instruction("ldrb w12, [sp, #128]"); // reload the conversion character +} + +/// Emits the AArch64 conversion dispatch. Only the conversion characters PHP defines are +/// accepted; anything else takes the controlled `ValueError` path rather than being handed +/// to libc, which is what keeps `%n` and other libc-only conversions unreachable. +fn emit_conversion_dispatch(emitter: &mut Emitter) { + emitter.instruction("cmp w12, #115"); // 's' string conversion? + emitter.instruction("b.eq __rt_sprintf_t_str"); // yes → string path + emitter.instruction("cmp w12, #100"); // 'd' signed decimal? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer path + emitter.instruction("cmp w12, #117"); // 'u' unsigned decimal? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer path + emitter.instruction("cmp w12, #111"); // 'o' octal? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer path + emitter.instruction("cmp w12, #120"); // 'x' lowercase hexadecimal? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer path + emitter.instruction("cmp w12, #88"); // 'X' uppercase hexadecimal? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer path + emitter.instruction("cmp w12, #98"); // 'b' binary? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer coercion, then the binary body + emitter.instruction("cmp w12, #99"); // 'c' single character? + emitter.instruction("b.eq __rt_sprintf_t_int"); // yes → integer coercion, then the single-byte body + emitter.instruction("cmp w12, #102"); // 'f' fixed-point? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("cmp w12, #70"); // 'F' locale-independent fixed-point? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("cmp w12, #101"); // 'e' scientific? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("cmp w12, #69"); // 'E' uppercase scientific? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("cmp w12, #103"); // 'g' shortest-of-e-or-f? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("cmp w12, #71"); // 'G' uppercase shortest-of-E-or-f? + emitter.instruction("b.eq __rt_sprintf_t_flt"); // yes → float path + emitter.instruction("b __rt_sprintf_sfatal"); // PHP rejects every other conversion +} + +/// Emits the AArch64 `%s` conversion. +/// +/// A string record is emitted straight from its pointer/length pair (so the result is +/// binary safe and not capped at any scratch-buffer size); precision truncates it. A +/// record carrying another tag is rendered numerically instead of being dereferenced. +fn emit_string_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_str"); + emitter.instruction("and x5, x4, #255"); // isolate the record type tag + emitter.instruction("cmp x5, #1"); // is this record actually a string? + emitter.instruction("b.ne __rt_sprintf_str_num"); // no → render the payload as a number + emitter.instruction("lsr x4, x4, #8"); // string byte length lives above the tag + emitter.instruction("cbnz x3, __rt_sprintf_str_ptr"); // a null pointer carries no bytes + emitter.instruction("mov x4, #0"); // treat a null string pointer as empty + emitter.label("__rt_sprintf_str_ptr"); + emitter.instruction("ldr x5, [sp, #104]"); // parsed precision + emitter.instruction("tbnz x5, #63, __rt_sprintf_emit"); // no precision → emit the whole string + emitter.instruction("cmp x4, x5"); // is the string already within the precision? + emitter.instruction("b.ls __rt_sprintf_emit"); // yes → emit it unchanged + emitter.instruction("mov x4, x5"); // truncate the string to the precision + emitter.instruction("b __rt_sprintf_emit"); // pad and copy the string body + + // -- non-string record under %s: format the payload instead of dereferencing it -- + emitter.label("__rt_sprintf_str_num"); + emitter.instruction("mov x9, #-1"); // the %s precision must not reach the numeric path + emitter.instruction("str x9, [sp, #104]"); // drop the string precision + emitter.instruction("cmp x5, #2"); // is the payload a double? + emitter.instruction("b.ne __rt_sprintf_str_int"); // no → render it as a signed integer + emitter.instruction("mov x9, #14"); // PHP renders floats with 14 significant digits + emitter.instruction("str x9, [sp, #104]"); // use that as the conversion precision + emitter.instruction("mov w12, #71"); // reuse the 'G' float conversion + emitter.instruction("str x12, [sp, #128]"); // record the substituted conversion character + emitter.instruction("b __rt_sprintf_t_flt"); // format through the float path + emitter.label("__rt_sprintf_str_int"); + emitter.instruction("mov w12, #100"); // reuse the 'd' integer conversion + emitter.instruction("str x12, [sp, #128]"); // record the substituted conversion character + emitter.instruction("b __rt_sprintf_t_int"); // format through the integer path +} + +/// Emits the AArch64 `%b` conversion body, which libc has no portable equivalent for. +/// Entered from the shared integer coercion with the operand already in `x3`. Digits are +/// generated backwards into the conversion scratch, so at most 64 bytes are written and the +/// result never carries leading zeros (PHP prints `0` for zero). +fn emit_binary_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_bin_go"); + emitter.instruction("add x9, sp, #264"); // write backwards from scratch + 72 bytes + emitter.instruction("mov x4, #0"); // generated digit count + emitter.label("__rt_sprintf_bin_loop"); + emitter.instruction("and x10, x3, #1"); // take the low bit of the remaining value + emitter.instruction("add w10, w10, #48"); // turn it into an ASCII digit + emitter.instruction("sub x9, x9, #1"); // step one byte back in the scratch + emitter.instruction("strb w10, [x9]"); // store the digit + emitter.instruction("add x4, x4, #1"); // count the digit + emitter.instruction("lsr x3, x3, #1"); // shift the value right by one bit + emitter.instruction("cbnz x3, __rt_sprintf_bin_loop"); // more bits → keep going + emitter.instruction("mov x3, x9"); // body pointer = first generated digit + emitter.instruction("b __rt_sprintf_emit"); // pad and copy the binary body +} + +/// Emits the AArch64 `%c` conversion body, entered from the shared integer coercion with the +/// operand already in `x3`. PHP appends the low byte of the argument and ignores width and +/// padding entirely, so the width slot is cleared before emitting. +fn emit_char_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_chr_go"); + emitter.instruction("add x9, sp, #192"); // reuse the conversion scratch for one byte + emitter.instruction("strb w3, [x9]"); // store the low byte of the argument + emitter.instruction("mov x3, x9"); // body pointer = the stored byte + emitter.instruction("mov x4, #1"); // body length = one byte + emitter.instruction("str xzr, [sp, #96]"); // PHP ignores width for %c + emitter.instruction("b __rt_sprintf_emit"); // copy the single byte +} + +/// Emits the AArch64 integer conversions (`%d`, `%u`, `%o`, `%x`, `%X`) plus the shared +/// operand coercion that `%b` and `%c` also enter through. +/// +/// The record tag decides the coercion: a double is truncated toward zero and a string is +/// parsed by `__rt_str_to_int`. Without that string case the helper would print the operand +/// pointer whenever the conversion character and the packed record disagree — which happens +/// for `v*printf()`, for a runtime-built format string, and for `%1$s`/`%1$d` on one +/// argument. The length handed to `__rt_str_to_int` is clamped to the C-string scratch. +/// +/// The C format string is assembled from the parsed flags rather than copied from the +/// program, so it is at most `"%+#llX"` plus a NUL. Precision is deliberately omitted: +/// PHP ignores it for integer conversions. +fn emit_integer_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_int"); + emitter.instruction("and x5, x4, #255"); // isolate the record type tag + emitter.instruction("cmp x5, #1"); // is the payload a string pointer? + emitter.instruction("b.eq __rt_sprintf_int_str"); // yes → parse it instead of printing the pointer + emitter.instruction("cmp x5, #2"); // is the payload a double? + emitter.instruction("b.ne __rt_sprintf_int_ready"); // no → the payload is already an integer + emitter.instruction("fmov d0, x3"); // move the double bits into an FP register + emitter.instruction("fcvtzs x3, d0"); // truncate the double toward zero like PHP + emitter.instruction("b __rt_sprintf_int_ready"); // the operand is an integer now + emitter.label("__rt_sprintf_int_str"); + emitter.instruction("mov x1, x3"); // string pointer for the numeric parse + emitter.instruction("lsr x2, x4, #8"); // string byte length for the numeric parse + emitter.instruction("cbz x1, __rt_sprintf_int_str_null"); // a null pointer parses as zero + emitter.instruction("cmp x2, #4095"); // __rt_cstr copies into a 4096-byte scratch + emitter.instruction("b.ls __rt_sprintf_int_str_go"); // the string already fits the C-string scratch + emitter.instruction("mov x2, #4095"); // clamp so the numeric prefix parse stays in bounds + emitter.label("__rt_sprintf_int_str_go"); + emitter.instruction("bl __rt_str_to_int"); // PHP leading-numeric string-to-int conversion + emitter.instruction("mov x3, x0"); // the parsed integer becomes the operand + emitter.instruction("b __rt_sprintf_int_ready"); // the operand is an integer now + emitter.label("__rt_sprintf_int_str_null"); + emitter.instruction("mov x3, #0"); // a null string operand formats as zero + emitter.label("__rt_sprintf_int_ready"); + emitter.instruction("ldrb w12, [sp, #128]"); // reload the conversion character after the parse + emitter.instruction("cmp w12, #98"); // is this the binary conversion? + emitter.instruction("b.eq __rt_sprintf_bin_go"); // yes → generate binary digits by hand + emitter.instruction("cmp w12, #99"); // is this the single-character conversion? + emitter.instruction("b.eq __rt_sprintf_chr_go"); // yes → emit the low byte directly + emitter.label("__rt_sprintf_int_go"); + emitter.instruction("add x14, sp, #160"); // mini C format cursor + emitter.instruction("mov w9, #37"); // '%' introducer + emitter.instruction("strb w9, [x14], #1"); // write the '%' introducer + emitter.instruction("cmp w12, #100"); // only 'd' is signed, so only it can force a sign + emitter.instruction("b.ne __rt_sprintf_int_noplus"); // other integer conversions ignore '+' + emitter.instruction("ldr x9, [sp, #112]"); // parsed flags + emitter.instruction("tbz x9, #1, __rt_sprintf_int_noplus"); // force-sign flag not set + emitter.instruction("mov w9, #43"); // '+' flag character + emitter.instruction("strb w9, [x14], #1"); // write the '+' flag + emitter.label("__rt_sprintf_int_noplus"); + emitter.instruction("cmp w12, #100"); // '#' is meaningless for 'd' + emitter.instruction("b.eq __rt_sprintf_int_noalt"); // skip the alternate-form flag + emitter.instruction("cmp w12, #117"); // '#' is meaningless for 'u' + emitter.instruction("b.eq __rt_sprintf_int_noalt"); // skip the alternate-form flag + emitter.instruction("ldr x9, [sp, #112]"); // parsed flags + emitter.instruction("tbz x9, #2, __rt_sprintf_int_noalt"); // alternate-form flag not set + emitter.instruction("mov w9, #35"); // '#' flag character + emitter.instruction("strb w9, [x14], #1"); // write the '#' flag + emitter.label("__rt_sprintf_int_noalt"); + emitter.instruction("mov w9, #108"); // 'l' length modifier character + emitter.instruction("strb w9, [x14], #1"); // write the first 'l' + emitter.instruction("strb w9, [x14], #1"); // write the second 'l' for a 64-bit operand + emitter.instruction("strb w12, [x14], #1"); // write the conversion character + emitter.instruction("strb wzr, [x14]"); // NUL-terminate the mini C format string + emitter.instruction("str x3, [sp]"); // first variadic slot (Apple AArch64 reads it here) + emitter.instruction("add x0, sp, #192"); // conversion scratch destination + emitter.instruction(&format!("mov x1, #{}", CONV_SCRATCH_CAP)); // conversion scratch capacity + emitter.instruction("add x2, sp, #160"); // the mini C format string + emitter.bl_c("snprintf"); // render the integer body through libc + emitter.instruction("b __rt_sprintf_snret"); // clamp and take the result - // -- load next arg (float bits) -- - emitter.instruction("lsl x15, x21, #4"); // arg offset = index * 16 - emitter.instruction("add x15, x22, x15"); // arg address in caller's stack - emitter.instruction("ldr x3, [x15]"); // load float bits as integer - emitter.instruction("add x21, x21, #1"); // increment arg index +} +/// Emits the AArch64 float conversions (`%f`, `%F`, `%e`, `%E`, `%g`, `%G`). +/// +/// The record tag decides the coercion: an int/bool payload is widened and a string is +/// parsed by `__rt_str_to_number`, so a mismatched record never reaches libc as raw pointer +/// bits. Precision is clamped to PHP's 53-digit maximum, which is what bounds the libc output +/// to the conversion scratch. `%f`/`%F`/`%e`/`%E` of negative zero print unsigned in PHP +/// (its own float renderer never emits the sign), while `%g`/`%G` keep it. +fn emit_float_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_flt"); + emitter.instruction("and x5, x4, #255"); // isolate the record type tag + emitter.instruction("cmp x5, #2"); // is the payload already a double? + emitter.instruction("b.eq __rt_sprintf_flt_bits"); // yes → use its bit pattern directly + emitter.instruction("cmp x5, #1"); // is the payload a string pointer? + emitter.instruction("b.eq __rt_sprintf_flt_str"); // yes → parse it instead of reading the pointer bits + emitter.instruction("scvtf d0, x3"); // widen an int/bool payload to a double + emitter.instruction("fmov x3, d0"); // keep the double bits in the integer register + emitter.instruction("b __rt_sprintf_flt_bits"); // the operand is a double now + emitter.label("__rt_sprintf_flt_str"); + emitter.instruction("mov x1, x3"); // string pointer for the numeric parse + emitter.instruction("lsr x2, x4, #8"); // string byte length for the numeric parse + emitter.instruction("cbz x1, __rt_sprintf_flt_str_null"); // a null pointer parses as zero + emitter.instruction("cmp x2, #4095"); // __rt_cstr copies into a 4096-byte scratch + emitter.instruction("b.ls __rt_sprintf_flt_str_go"); // the string already fits the C-string scratch + emitter.instruction("mov x2, #4095"); // clamp so the numeric prefix parse stays in bounds + emitter.label("__rt_sprintf_flt_str_go"); + emitter.instruction("bl __rt_str_to_number"); // PHP leading-numeric string-to-float conversion + emitter.instruction("fmov x3, d0"); // keep the parsed double bits in the integer register + emitter.instruction("ldrb w12, [sp, #128]"); // reload the conversion character after the parse + emitter.instruction("b __rt_sprintf_flt_bits"); // the operand is a double now + emitter.label("__rt_sprintf_flt_str_null"); + emitter.instruction("mov x3, #0"); // a null string operand formats as zero + emitter.label("__rt_sprintf_flt_bits"); + emitter.instruction("cmp w12, #103"); // 'g' keeps PHP's negative-zero sign + emitter.instruction("b.eq __rt_sprintf_flt_nz"); // skip the negative-zero normalization + emitter.instruction("cmp w12, #71"); // 'G' keeps PHP's negative-zero sign + emitter.instruction("b.eq __rt_sprintf_flt_nz"); // skip the negative-zero normalization + emitter.instruction("lsl x9, x3, #1"); // drop the sign bit to test for any zero + emitter.instruction("cbnz x9, __rt_sprintf_flt_nz"); // not a zero → leave the value alone + emitter.instruction("mov x3, #0"); // PHP prints -0.0 as 0.000000 under %f/%e + emitter.label("__rt_sprintf_flt_nz"); + emitter.instruction("add x14, sp, #160"); // mini C format cursor + emitter.instruction("mov w9, #37"); // '%' introducer + emitter.instruction("strb w9, [x14], #1"); // write the '%' introducer + emitter.instruction("ldr x9, [sp, #112]"); // parsed flags + emitter.instruction("tbz x9, #1, __rt_sprintf_flt_noplus"); // force-sign flag not set + emitter.instruction("mov w9, #43"); // '+' flag character + emitter.instruction("strb w9, [x14], #1"); // write the '+' flag + emitter.label("__rt_sprintf_flt_noplus"); + emitter.instruction("ldr x9, [sp, #112]"); // parsed flags + emitter.instruction("tbz x9, #2, __rt_sprintf_flt_noalt"); // alternate-form flag not set + emitter.instruction("mov w9, #35"); // '#' flag character + emitter.instruction("strb w9, [x14], #1"); // write the '#' flag + emitter.label("__rt_sprintf_flt_noalt"); + emitter.instruction("ldr x5, [sp, #104]"); // parsed precision + emitter.instruction("tbnz x5, #63, __rt_sprintf_flt_noprec"); // absent → libc's default of six digits + emitter.instruction("cmp x5, #53"); // PHP caps float precision at 53 digits + emitter.instruction("b.ls __rt_sprintf_flt_precok"); // within the cap + emitter.instruction("mov x5, #53"); // clamp to PHP's maximum precision + emitter.label("__rt_sprintf_flt_precok"); + emitter.instruction("mov w9, #46"); // '.' precision introducer + emitter.instruction("strb w9, [x14], #1"); // write the '.' introducer + emitter.instruction("cmp x5, #10"); // does the precision need two digits? + emitter.instruction("b.lo __rt_sprintf_flt_prec1"); // no → a single digit is enough + emitter.instruction("mov x9, #10"); // decimal radix for the split + emitter.instruction("udiv x10, x5, x9"); // tens digit of the clamped precision + emitter.instruction("msub x11, x10, x9, x5"); // units digit of the clamped precision + emitter.instruction("add w10, w10, #48"); // turn the tens digit into ASCII + emitter.instruction("strb w10, [x14], #1"); // write the tens digit + emitter.instruction("add w11, w11, #48"); // turn the units digit into ASCII + emitter.instruction("strb w11, [x14], #1"); // write the units digit + emitter.instruction("b __rt_sprintf_flt_noprec"); // precision written + emitter.label("__rt_sprintf_flt_prec1"); + emitter.instruction("add w5, w5, #48"); // turn the single digit into ASCII + emitter.instruction("strb w5, [x14], #1"); // write the single precision digit + emitter.label("__rt_sprintf_flt_noprec"); + emitter.instruction("strb w12, [x14], #1"); // write the conversion character + emitter.instruction("strb wzr, [x14]"); // NUL-terminate the mini C format string + emitter.instruction("str x3, [sp]"); // first variadic slot (Apple AArch64 reads it here) if emitter.platform == Platform::Linux { - emitter.instruction("fmov d0, x3"); // pass first variadic double in the Linux AArch64 FP register + emitter.instruction("fmov d0, x3"); // Linux AArch64 passes the first FP variadic in d0 } + emitter.instruction("add x0, sp, #192"); // conversion scratch destination + emitter.instruction(&format!("mov x1, #{}", CONV_SCRATCH_CAP)); // conversion scratch capacity + emitter.instruction("add x2, sp, #160"); // the mini C format string + emitter.bl_c("snprintf"); // render the float body through libc + emitter.instruction("b __rt_sprintf_snret"); // clamp and take the result +} - // -- store variadic arg on stack for snprintf -- - emitter.instruction("str x3, [sp]"); // variadic float bits at [sp] +/// Emits the AArch64 post-`snprintf` clamp. +/// +/// libc returns the number of bytes it *would* have written, so the value is clamped to +/// the bytes actually present in the scratch buffer before it is ever used as a length. +/// That clamp is the direct fix for the out-of-bounds stack read this helper used to have. +fn emit_snprintf_result(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_snret"); + emitter.instruction("sxtw x4, w0"); // snprintf returns a signed 32-bit count + emitter.instruction("tbz x4, #63, __rt_sprintf_snret_nn"); // non-negative → usable as a length + emitter.instruction("mov x4, #0"); // an encoding error produced no bytes + emitter.label("__rt_sprintf_snret_nn"); + emitter.instruction(&format!("cmp x4, #{}", CONV_SCRATCH_CAP - 1)); // did libc want more than the scratch holds? + emitter.instruction("b.ls __rt_sprintf_snret_ok"); // no → every counted byte is really there + emitter.instruction(&format!("mov x4, #{}", CONV_SCRATCH_CAP - 1)); // clamp to the bytes actually written + emitter.label("__rt_sprintf_snret_ok"); + emitter.instruction("add x3, sp, #192"); // body pointer = conversion scratch + emitter.instruction("ldr x9, [sp, #128]"); // reload the conversion character + emitter.instruction("cmp w9, #101"); // 'e' needs PHP's exponent form + emitter.instruction("b.eq __rt_sprintf_expfix"); // compact the exponent + emitter.instruction("cmp w9, #69"); // 'E' needs PHP's exponent form + emitter.instruction("b.eq __rt_sprintf_expfix"); // compact the exponent + emitter.instruction("b __rt_sprintf_emit"); // pad and copy the rendered body +} - // -- call snprintf(buf, 128, fmt) with variadic float on stack -- - emitter.instruction("add x0, sp, #112"); // output buffer at sp+112 - emitter.instruction("mov x1, #128"); // buffer size - emitter.instruction("add x2, sp, #80"); // mini format string at sp+80 - emitter.bl_c("snprintf"); // call libc snprintf - // x0 = number of chars written +/// Emits the AArch64 exponent compaction for `%e`/`%E`. +/// +/// C always pads the exponent to at least two digits (`1.234568e+04`) while PHP does not +/// (`1.234568e+4`), so the leading zeros of the exponent field are removed in place, always +/// leaving at least one digit. +fn emit_exponent_compaction(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_expfix"); + emitter.instruction("mov x9, x3"); // read cursor over the rendered body + emitter.instruction("mov x10, x3"); // write cursor for the compacted body + emitter.instruction("add x11, x3, x4"); // one past the last rendered byte + emitter.label("__rt_sprintf_expfix_scan"); + emitter.instruction("cmp x9, x11"); // reached the end without an exponent? + emitter.instruction("b.hs __rt_sprintf_expfix_done"); // yes → nothing to compact + emitter.instruction("ldrb w13, [x9]"); // load the current body byte + emitter.instruction("cmp w13, #101"); // lowercase exponent marker? + emitter.instruction("b.eq __rt_sprintf_expfix_hit"); // yes → compact from here + emitter.instruction("cmp w13, #69"); // uppercase exponent marker? + emitter.instruction("b.eq __rt_sprintf_expfix_hit"); // yes → compact from here + emitter.instruction("strb w13, [x10]"); // keep the mantissa byte + emitter.instruction("add x9, x9, #1"); // advance the read cursor + emitter.instruction("add x10, x10, #1"); // advance the write cursor + emitter.instruction("b __rt_sprintf_expfix_scan"); // keep scanning for the exponent + emitter.label("__rt_sprintf_expfix_hit"); + emitter.instruction("strb w13, [x10]"); // keep the exponent marker + emitter.instruction("add x9, x9, #1"); // advance the read cursor + emitter.instruction("add x10, x10, #1"); // advance the write cursor + emitter.instruction("cmp x9, x11"); // is there anything after the marker? + emitter.instruction("b.hs __rt_sprintf_expfix_done"); // no → the body ends here + emitter.instruction("ldrb w13, [x9]"); // load the exponent sign byte + emitter.instruction("cmp w13, #43"); // '+' exponent sign? + emitter.instruction("b.eq __rt_sprintf_expfix_sign"); // yes → keep it + emitter.instruction("cmp w13, #45"); // '-' exponent sign? + emitter.instruction("b.ne __rt_sprintf_expfix_zeros"); // no sign at all → go straight to the digits + emitter.label("__rt_sprintf_expfix_sign"); + emitter.instruction("strb w13, [x10]"); // keep the exponent sign + emitter.instruction("add x9, x9, #1"); // advance the read cursor + emitter.instruction("add x10, x10, #1"); // advance the write cursor + emitter.label("__rt_sprintf_expfix_zeros"); + emitter.instruction("sub x15, x11, #1"); // index of the final exponent digit + emitter.label("__rt_sprintf_expfix_zloop"); + emitter.instruction("cmp x9, x15"); // never drop the last exponent digit + emitter.instruction("b.hs __rt_sprintf_expfix_tail"); // one digit left → stop stripping + emitter.instruction("ldrb w13, [x9]"); // load the current exponent digit + emitter.instruction("cmp w13, #48"); // is it a padding zero? + emitter.instruction("b.ne __rt_sprintf_expfix_tail"); // no → the exponent starts here + emitter.instruction("add x9, x9, #1"); // skip the padding zero + emitter.instruction("b __rt_sprintf_expfix_zloop"); // check the next exponent digit + emitter.label("__rt_sprintf_expfix_tail"); + emitter.instruction("cmp x9, x11"); // copied every remaining byte? + emitter.instruction("b.hs __rt_sprintf_expfix_done"); // yes → compaction finished + emitter.instruction("ldrb w13, [x9]"); // load the next exponent byte + emitter.instruction("strb w13, [x10]"); // keep the exponent byte + emitter.instruction("add x9, x9, #1"); // advance the read cursor + emitter.instruction("add x10, x10, #1"); // advance the write cursor + emitter.instruction("b __rt_sprintf_expfix_tail"); // copy the rest of the exponent + emitter.label("__rt_sprintf_expfix_done"); + emitter.instruction("sub x4, x10, x3"); // compacted body length +} + +/// Emits the AArch64 pad-and-copy stage shared by every conversion. +/// +/// `x3`/`x4` carry the conversion body. The field width is validated against PHP's +/// `0..INT_MAX` range and the whole padded result is bounds-checked against the end of +/// `_concat_buf` *before* a single byte is written, so neither an absurd width nor a +/// long body can walk off the arena. Zero padding is inserted after a leading sign, +/// matching PHP's `sprintf("%05d", -42)` → `-0042`. +fn emit_pad_and_copy(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_emit"); + emitter.instruction("ldr x5, [sp, #96]"); // parsed field width + emitter.instruction("lsr x9, x5, #31"); // any bit above INT_MAX set? + emitter.instruction("cbnz x9, __rt_sprintf_wfatal"); // yes → PHP rejects the width + emitter.instruction("mov x11, #0"); // padding byte count + emitter.instruction("cmp x5, x4"); // is the body already at least as wide? + emitter.instruction("b.ls __rt_sprintf_emit_nopad"); // yes → no padding needed + emitter.instruction("sub x11, x5, x4"); // padding = width - body length + emitter.label("__rt_sprintf_emit_nopad"); + emitter.instruction("add x13, x4, x11"); // total bytes this conversion emits + emitter.instruction("add x13, x13, x23"); // address just past the emitted bytes + emitter.instruction("ldr x15, [sp, #144]"); // concat-buffer write limit + emitter.instruction("cmp x13, x15"); // would the conversion leave the arena? + emitter.instruction("b.hi __rt_sprintf_ofatal"); // yes → controlled fatal instead of an overrun + emitter.instruction("ldr x9, [sp, #120]"); // pad character + emitter.instruction("ldr x10, [sp, #112]"); // parsed flags + emitter.instruction("tbnz x10, #0, __rt_sprintf_emit_left"); // left-aligned → body first, padding after + emitter.instruction("cbz x11, __rt_sprintf_emit_pad"); // no padding → copy the body directly + emitter.instruction("cmp w9, #48"); // only '0' padding moves ahead of the sign + emitter.instruction("b.ne __rt_sprintf_emit_pad"); // other pad characters stay before the sign + emitter.instruction("cbz x4, __rt_sprintf_emit_pad"); // an empty body has no sign to hoist + emitter.instruction("ldrb w13, [x3]"); // first body byte + emitter.instruction("cmp w13, #45"); // is it a minus sign? + emitter.instruction("b.eq __rt_sprintf_emit_sign"); // yes → emit it before the zeros + emitter.instruction("cmp w13, #43"); // is it a plus sign? + emitter.instruction("b.ne __rt_sprintf_emit_pad"); // no sign → pad normally + emitter.label("__rt_sprintf_emit_sign"); + emitter.instruction("strb w13, [x23], #1"); // emit the sign ahead of the zero padding + emitter.instruction("add x3, x3, #1"); // the sign is no longer part of the body + emitter.instruction("sub x4, x4, #1"); // shorten the body accordingly + emitter.label("__rt_sprintf_emit_pad"); + emitter.instruction("cbz x11, __rt_sprintf_emit_copy"); // padding written → copy the body + emitter.instruction("strb w9, [x23], #1"); // emit one padding byte + emitter.instruction("sub x11, x11, #1"); // one padding byte fewer to write + emitter.instruction("b __rt_sprintf_emit_pad"); // keep padding + emitter.label("__rt_sprintf_emit_copy"); + emitter.instruction("cbz x4, __rt_sprintf_loop"); // body copied → scan the next format byte + emitter.instruction("ldrb w13, [x3], #1"); // load the next body byte + emitter.instruction("strb w13, [x23], #1"); // emit the body byte + emitter.instruction("sub x4, x4, #1"); // one body byte fewer to copy + emitter.instruction("b __rt_sprintf_emit_copy"); // keep copying + emitter.label("__rt_sprintf_emit_left"); + emitter.instruction("cbz x4, __rt_sprintf_emit_lpad"); // body copied → append the padding + emitter.instruction("ldrb w13, [x3], #1"); // load the next body byte + emitter.instruction("strb w13, [x23], #1"); // emit the body byte + emitter.instruction("sub x4, x4, #1"); // one body byte fewer to copy + emitter.instruction("b __rt_sprintf_emit_left"); // keep copying + emitter.label("__rt_sprintf_emit_lpad"); + emitter.instruction("cbz x11, __rt_sprintf_loop"); // padding written → scan the next format byte + emitter.instruction("strb w9, [x23], #1"); // emit one trailing padding byte + emitter.instruction("sub x11, x11, #1"); // one padding byte fewer to write + emitter.instruction("b __rt_sprintf_emit_lpad"); // keep padding +} - // -- copy snprintf result to concat_buf -- - emitter.instruction("mov x4, x0"); // chars to copy - emitter.instruction("add x3, sp, #112"); // source buffer +/// Emits the four AArch64 controlled-fatal exits: out-of-range width, result larger than +/// the concat arena, too few arguments, and an unknown conversion character. Each writes a +/// PHP-shaped diagnostic to stderr and exits with PHP's fatal-error status (255). +fn emit_fatal_paths(emitter: &mut Emitter) { + emit_fatal(emitter, "__rt_sprintf_wfatal", "_sprintf_width_msg", SPRINTF_WIDTH_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_ofatal", "_sprintf_overflow_msg", SPRINTF_OVERFLOW_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_afatal", "_sprintf_argcount_msg", SPRINTF_ARGCOUNT_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_sfatal", "_sprintf_unknown_spec_msg", SPRINTF_UNKNOWN_SPEC_MSG.len()); +} - emitter.label("__rt_sprintf_copy_f"); - emitter.instruction("cbz x4, __rt_sprintf_copy_f_done"); // no bytes left - emitter.instruction("ldrb w15, [x3], #1"); // load byte from snprintf output - emitter.instruction("strb w15, [x23], #1"); // write to concat_buf - emitter.instruction("sub x4, x4, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_copy_f"); // continue copying +/// Emits one AArch64 fatal exit block: write `len` bytes of `symbol` to stderr, then exit +/// with status 255 (the status PHP uses for an uncaught fatal error). +fn emit_fatal(emitter: &mut Emitter, label: &str, symbol: &str, len: usize) { + emitter.label(label); + emitter.instruction("mov x0, #2"); // write the diagnostic to stderr + abi::emit_symbol_address(emitter, "x1", symbol); + emitter.instruction(&format!("mov x2, #{}", len)); // exact diagnostic byte length + emitter.syscall(4); + emitter.instruction("mov x0, #255"); // PHP exits with 255 on a fatal error + emitter.syscall(1); +} - emitter.label("__rt_sprintf_copy_f_done"); - emitter.instruction("b __rt_sprintf_loop"); // next format char +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::Target; - // ================================================================ - // INTEGER: %d, %x, %o, %c, etc. (with optional flags/width/precision) - // Uses %lld/%llx/%llo for 64-bit ints (except %c which stays 32-bit). - // Passes the integer value on the stack at [sp] for variadic ABI. - // ================================================================ - emitter.label("__rt_sprintf_type_int"); - - // For 'd', 'x', 'o' we need 'll' prefix for 64-bit; 'c' stays as-is - emitter.instruction("cmp w12, #99"); // 'c' ? - emitter.instruction("b.eq __rt_sprintf_int_noprefix"); // skip 'll' for %c - - // Write 'll' length modifier for 64-bit integer types - emitter.instruction("mov w15, #108"); // 'l' character - emitter.instruction("strb w15, [x10], #1"); // write first 'l' to mini buffer - emitter.instruction("strb w15, [x10], #1"); // write second 'l' to mini buffer - - emitter.label("__rt_sprintf_int_noprefix"); - emitter.instruction("strb w12, [x10], #1"); // copy type char to mini buffer - emitter.instruction("strb wzr, [x10]"); // null-terminate format string - - // -- load next arg (int value) -- - emitter.instruction("lsl x15, x21, #4"); // arg offset = index * 16 - emitter.instruction("add x15, x22, x15"); // arg address in caller's stack - emitter.instruction("ldr x3, [x15]"); // load integer value - emitter.instruction("add x21, x21, #1"); // increment arg index - - // -- store variadic arg on stack for snprintf -- - emitter.instruction("str x3, [sp]"); // variadic int at [sp] - - // -- call snprintf(buf, 128, fmt) with variadic int on stack -- - emitter.instruction("add x0, sp, #112"); // output buffer at sp+112 - emitter.instruction("mov x1, #128"); // buffer size - emitter.instruction("add x2, sp, #80"); // mini format string at sp+80 - emitter.bl_c("snprintf"); // call libc snprintf - // x0 = number of chars written - - // -- copy snprintf result to concat_buf -- - emitter.instruction("mov x4, x0"); // chars to copy - emitter.instruction("add x3, sp, #112"); // source buffer - - emitter.label("__rt_sprintf_copy_i"); - emitter.instruction("cbz x4, __rt_sprintf_copy_i_done"); // no bytes left - emitter.instruction("ldrb w15, [x3], #1"); // load byte from snprintf output - emitter.instruction("strb w15, [x23], #1"); // write to concat_buf - emitter.instruction("sub x4, x4, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_copy_i"); // continue copying - - emitter.label("__rt_sprintf_copy_i_done"); - emitter.instruction("b __rt_sprintf_loop"); // next format char + /// Emits `__rt_sprintf` for one target and returns the assembly text. + fn sprintf_asm(target: Target) -> String { + let mut emitter = Emitter::new(target); + emit_sprintf(&mut emitter); + emitter.output() + } - // ================================================================ - // STRING: %s (with optional width/padding) - // snprintf needs a null-terminated C string. Our strings are ptr+len, - // so we copy the string to a temp buffer at sp+240 and null-terminate it. - // The variadic pointer goes on the stack at [sp]. - // ================================================================ - emitter.label("__rt_sprintf_type_str"); - emitter.instruction("strb w12, [x10], #1"); // copy 's' to mini buffer - emitter.instruction("strb wzr, [x10]"); // null-terminate format string - - // -- load next arg (string: ptr + tag|len) -- - emitter.instruction("lsl x15, x21, #4"); // arg offset = index * 16 - emitter.instruction("add x15, x22, x15"); // arg address in caller's stack - emitter.instruction("ldr x3, [x15]"); // load string pointer - emitter.instruction("ldr x4, [x15, #8]"); // load tag|length word - emitter.instruction("lsr x4, x4, #8"); // extract length (shift right 8) - emitter.instruction("add x21, x21, #1"); // increment arg index - - // -- copy string to temp buffer at sp+240 and null-terminate -- - // Limit copy to 127 bytes to fit in our 128-byte buffer - emitter.instruction("cmp x4, #127"); // string longer than buffer? - emitter.instruction("b.le __rt_sprintf_str_len_ok"); // no → use actual length - emitter.instruction("mov x4, #127"); // clamp to 127 bytes - - emitter.label("__rt_sprintf_str_len_ok"); - emitter.instruction("add x6, sp, #240"); // temp buffer for null-terminated copy - emitter.instruction("mov x7, x4"); // bytes to copy - - emitter.label("__rt_sprintf_strcopy"); - emitter.instruction("cbz x7, __rt_sprintf_strcopy_done"); // done copying - emitter.instruction("ldrb w15, [x3], #1"); // load source byte - emitter.instruction("strb w15, [x6], #1"); // write to temp buffer - emitter.instruction("sub x7, x7, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_strcopy"); // continue copying - - emitter.label("__rt_sprintf_strcopy_done"); - emitter.instruction("strb wzr, [x6]"); // null-terminate the copy - - // -- store variadic arg (pointer to null-terminated copy) on stack -- - emitter.instruction("add x3, sp, #240"); // pointer to null-terminated string - emitter.instruction("str x3, [sp]"); // variadic string ptr at [sp] - - // -- call snprintf(buf, 128, fmt) with variadic string ptr on stack -- - emitter.instruction("add x0, sp, #112"); // output buffer at sp+112 - emitter.instruction("mov x1, #128"); // buffer size - emitter.instruction("add x2, sp, #80"); // mini format string at sp+80 - emitter.bl_c("snprintf"); // call libc snprintf - // x0 = number of chars written - - // -- copy snprintf result to concat_buf -- - emitter.instruction("mov x4, x0"); // chars to copy - emitter.instruction("add x3, sp, #112"); // source buffer - - emitter.label("__rt_sprintf_copy_s"); - emitter.instruction("cbz x4, __rt_sprintf_copy_s_done"); // no bytes left - emitter.instruction("ldrb w15, [x3], #1"); // load byte from snprintf output - emitter.instruction("strb w15, [x23], #1"); // write to concat_buf - emitter.instruction("sub x4, x4, #1"); // decrement counter - emitter.instruction("b __rt_sprintf_copy_s"); // continue copying - - emitter.label("__rt_sprintf_copy_s_done"); - emitter.instruction("b __rt_sprintf_loop"); // next format char + /// Both lowerings must clamp the `snprintf` return value to the bytes that are really + /// present in the conversion scratch. Copying `snprintf`'s "would have written" count + /// out of a fixed buffer is what leaked stack memory into `sprintf()` results. + #[test] + fn snprintf_return_is_clamped_to_the_scratch_buffer() { + let arm = sprintf_asm(Target::new(Platform::MacOS, Arch::AArch64)); + assert!(arm.contains("sxtw x4, w0"), "{arm}"); + assert!(arm.contains(&format!("cmp x4, #{}", CONV_SCRATCH_CAP - 1)), "{arm}"); + assert!(arm.contains(&format!("mov x4, #{}", CONV_SCRATCH_CAP - 1)), "{arm}"); + + let x64 = sprintf_asm(Target::new(Platform::Linux, Arch::X86_64)); + assert!(x64.contains("movsxd r11, eax"), "{x64}"); + assert!(x64.contains(&format!("cmp r11, {}", CONV_SCRATCH_CAP - 1)), "{x64}"); + assert!(x64.contains(&format!("mov r11d, {}", CONV_SCRATCH_CAP - 1)), "{x64}"); + } - // ================================================================ - // DONE: finalize result and clean up - // ================================================================ - emitter.label("__rt_sprintf_done"); - emitter.instruction("mov x1, x24"); // result start ptr in concat_buf - emitter.instruction("sub x2, x23, x24"); // result length + /// Both lowerings must bound every conversion against the end of `_concat_buf` and + /// reject widths outside PHP's `0..INT_MAX` range instead of writing past the arena. + #[test] + fn writes_are_bounded_and_absurd_widths_are_rejected() { + let arm = sprintf_asm(Target::new(Platform::MacOS, Arch::AArch64)); + assert!(arm.contains("b.hi __rt_sprintf_ofatal"), "{arm}"); + assert!(arm.contains("b.hs __rt_sprintf_ofatal"), "{arm}"); + assert!(arm.contains("lsr x9, x5, #31"), "{arm}"); + assert!(arm.contains("cbnz x9, __rt_sprintf_wfatal"), "{arm}"); + assert!(arm.contains("b.hs __rt_sprintf_afatal"), "{arm}"); + + let x64 = sprintf_asm(Target::new(Platform::Linux, Arch::X86_64)); + assert!(x64.contains("ja __rt_sprintf_ofatal_x64"), "{x64}"); + assert!(x64.contains("jae __rt_sprintf_ofatal_x64"), "{x64}"); + assert!(x64.contains("shr rcx, 31"), "{x64}"); + assert!(x64.contains("jnz __rt_sprintf_wfatal_x64"), "{x64}"); + assert!(x64.contains("jae __rt_sprintf_afatal_x64"), "{x64}"); + } - // -- update concat_off -- - emitter.instruction("ldr x8, [x25]"); // current concat offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x25]"); // store updated offset - - // -- prepare to pop args from caller's stack -- - emitter.instruction("mov x0, x26"); // arg_count - emitter.instruction("lsl x0, x0, #4"); // bytes = count * 16 - - // -- restore callee-saved registers -- - emitter.instruction("ldp x19, x20, [sp, #16]"); // restore x19, x20 - emitter.instruction("ldp x21, x22, [sp, #32]"); // restore x21, x22 - emitter.instruction("ldp x23, x24, [sp, #48]"); // restore x23, x24 - emitter.instruction("ldp x25, x26, [sp, #64]"); // restore x25, x26 - emitter.instruction("ldp x29, x30, [sp, #368]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #384"); // deallocate our frame - emitter.instruction("add sp, sp, x0"); // pop caller's args from stack - emitter.instruction("ret"); // return + /// The C format string handed to libc is assembled from parsed state, so an unknown + /// conversion character must reach the `ValueError` exit rather than `snprintf`. This is + /// what keeps `%n` — an arbitrary-write primitive — unreachable from PHP source. + #[test] + fn unknown_conversions_never_reach_libc() { + let arm = sprintf_asm(Target::new(Platform::MacOS, Arch::AArch64)); + assert!(arm.contains("b __rt_sprintf_sfatal"), "{arm}"); + assert!(arm.contains("_sprintf_unknown_spec_msg"), "{arm}"); + + let x64 = sprintf_asm(Target::new(Platform::Linux, Arch::X86_64)); + assert!(x64.contains("jmp __rt_sprintf_sfatal_x64"), "{x64}"); + assert!(x64.contains("_sprintf_unknown_spec_msg"), "{x64}"); + } } diff --git a/src/codegen_support/runtime/strings/sprintf_x86_64.rs b/src/codegen_support/runtime/strings/sprintf_x86_64.rs index 0fb6165e50..c3166f0420 100644 --- a/src/codegen_support/runtime/strings/sprintf_x86_64.rs +++ b/src/codegen_support/runtime/strings/sprintf_x86_64.rs @@ -1,252 +1,815 @@ //! Purpose: -//! Emits the `__rt_sprintf`, `__rt_sprintf_loop_linux_x86_64` runtime helper assembly for Linux x86_64 sprintf formatting. -//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! Emits the Linux x86_64 lowering of `__rt_sprintf`, the shared PHP `printf`-family +//! formatter. It is the exact behavioural mirror of the AArch64 lowering in +//! `sprintf.rs`; the two must be changed together. //! //! Called from: -//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! - `crate::codegen_support::runtime::strings::sprintf::emit_sprintf()`, which dispatches +//! here for `Arch::X86_64`. //! //! Key details: -//! - Formatting helpers parse format strings and marshal values through target ABI calls or emitted formatting paths. +//! - Specifiers are parsed into frame slots (argument number, flags, pad character, width, +//! precision, conversion character). No program-supplied byte is ever copied into the C +//! format string handed to libc, so an over-long specifier cannot overrun the mini format +//! buffer and an unknown conversion (notably `%n`) never reaches `snprintf`. +//! - Padding is applied by this helper; libc only renders the unpadded numeric body into a +//! 512-byte scratch, bounded because precision is clamped to PHP's 53-digit maximum. +//! `%s`, `%b`, and `%c` bypass libc entirely. +//! - Every write into `_concat_buf` is bounds-checked against the end of the 64 KiB arena. +//! - Each conversion coerces its operand from the record's type tag (double↔int, and +//! string→number through `__rt_str_to_int` / `__rt_str_to_number`), so a record whose tag +//! disagrees with the conversion character is converted, never printed as a raw pointer. +//! - All parse state lives in the frame because every SysV temporary is clobbered by the +//! `snprintf` call; only `rbx`, `r12`-`r15` survive it. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; +use crate::codegen_support::runtime::data::{ + SPRINTF_ARGCOUNT_MSG, SPRINTF_OVERFLOW_MSG, SPRINTF_UNKNOWN_SPEC_MSG, SPRINTF_WIDTH_MSG, +}; + +use super::sprintf::{CONCAT_BUF_CAP, CONV_SCRATCH_CAP}; /// Emits the `__rt_sprintf` runtime helper for Linux x86_64. /// -/// ## Register contract on entry -/// - `rax`: pointer to the current format string -/// - `rdx`: remaining format string length in bytes -/// - `rdi`: packed variadic argument count -/// - `rsi`: pointer to caller-owned packed variadic argument records on the stack -/// -/// ## Register contract on exit -/// - `rax`: pointer to the formatted string within the concat buffer -/// - `rdx`: length of the formatted string in bytes +/// # Register contract on entry +/// - `rdi`: number of packed variadic argument records pushed by the caller +/// - `rax`: format string pointer +/// - `rdx`: format string byte length +/// - caller stack above the return address: `rdi` records of 16 bytes, `[payload, tag]` /// -/// ## Operation -/// Scans the format string byte-by-byte. Literal bytes are copied directly to the concat -/// buffer. `'%'` introduces a specifier: flags, width, precision, and type are parsed into a -/// local mini format string, then `snprintf` is invoked to format one argument. The result is -/// copied into the concat buffer and the format scan resumes. +/// # Register contract on exit +/// - `rax`: result pointer inside `_concat_buf` +/// - `rdx`: result byte length /// -/// Supported type characters: `%f`, `%e`, `%g` (float via `xmm0`), `%s` (string), `%c` (char), -/// and integer-like types via `snprintf`. `%%` emits a literal `'%'`. +/// The record tag word is `0` for int, `1 | (len << 8)` for string, `2` for float and `3` +/// for bool; the helper consults it so a conversion never dereferences a payload that is +/// not a string pointer. `_concat_off` is advanced by the result length and the caller's +/// tagged records are discarded by rethreading the stack before `ret`. /// -/// The concat-buffer write cursor is advanced and published to the `_concat_off` symbol so -///串联 `printf` calls accumulate into a single output buffer. Callee-saved registers -/// (`rbx`, `r12`–`r15`, `rbp`) are preserved across the helper; `r11` and `rcx` are used as -/// temporaries. Caller-owned tagged variadic records are discarded before returning. +/// Callee-saved registers used: `rbx` = write cursor in `_concat_buf`, `r12` = format +/// cursor, `r13` = remaining format bytes, `r14` = next sequential argument index, +/// `r15` = argument record base. pub(super) fn emit_sprintf_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: sprintf ---"); emitter.label_global("__rt_sprintf"); - emitter.instruction("push rbp"); // preserve the caller frame pointer before reserving sprintf() local storage - emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the format cursor, variadic cursor, and scratch buffers - emitter.instruction("push rbx"); // preserve the concat-buffer destination cursor across nested snprintf and cstr helper calls - emitter.instruction("push r12"); // preserve the format-string pointer across nested helper calls - emitter.instruction("push r13"); // preserve the remaining format-string length across nested helper calls - emitter.instruction("push r14"); // preserve the packed variadic argument index across nested helper calls - emitter.instruction("push r15"); // preserve the caller-stack variadic base pointer across nested helper calls - emitter.instruction("sub rsp, 328"); // reserve aligned local storage for the mini format buffer, snprintf output buffer, and temporary C string copy - emitter.instruction("mov r12, rax"); // preserve the current format-string pointer across the whole sprintf scan loop - emitter.instruction("mov r13, rdx"); // preserve the remaining format-string length across the whole sprintf scan loop - emitter.instruction("xor r14d, r14d"); // start consuming packed variadic argument records from logical index zero - emitter.instruction("lea r15, [rbp + 16]"); // point at the caller-owned packed variadic argument records that begin above the saved return address - emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // preserve the packed variadic argument count so the helper can discard the caller records before returning - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_off"); - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current concat-buffer write cursor before appending the formatted output - crate::codegen_support::abi::emit_symbol_address(emitter, "rcx", "_concat_buf"); - emitter.instruction("lea rbx, [rcx + r11]"); // compute the concat-buffer destination cursor where the formatted output will begin - emitter.instruction("mov QWORD PTR [rbp - 48], rbx"); // preserve the concat-buffer start pointer for the final x86_64 string return pair - emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // preserve the concat-offset symbol address so the helper can publish the new write cursor - - emitter.label("__rt_sprintf_loop_linux_x86_64"); - emitter.instruction("test r13, r13"); // has the entire format string been consumed already? - emitter.instruction("jz __rt_sprintf_done_linux_x86_64"); // stop scanning once the format string is exhausted - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the next format byte before deciding whether it is literal text or a format specifier - emitter.instruction("add r12, 1"); // advance the format cursor after consuming one format byte - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming one format byte - emitter.instruction("cmp r8b, 37"); // is the current format byte the '%' introducer of a format specifier? - emitter.instruction("je __rt_sprintf_fmt_linux_x86_64"); // branch into format-specifier parsing when the current format byte is '%' - emitter.instruction("mov BYTE PTR [rbx], r8b"); // copy the literal format byte directly into the concat-buffer destination cursor - emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after copying one literal byte - emitter.instruction("jmp __rt_sprintf_loop_linux_x86_64"); // continue scanning the remaining format string after copying a literal byte - - emitter.label("__rt_sprintf_fmt_linux_x86_64"); - emitter.instruction("test r13, r13"); // is the format string exhausted immediately after the '%' introducer? - emitter.instruction("jz __rt_sprintf_done_linux_x86_64"); // stop scanning when a trailing '%' lacks a following type character - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the next format byte before deciding between '%%' and a typed specifier - emitter.instruction("cmp r8b, 37"); // is the current format sequence '%%' for a literal percent sign? - emitter.instruction("jne __rt_sprintf_scan_spec_linux_x86_64"); // fall through to typed specifier scanning when the current format sequence is not '%%' - emitter.instruction("add r12, 1"); // consume the second '%' byte after recognizing the literal percent escape - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming the literal percent escape - emitter.instruction("mov BYTE PTR [rbx], r8b"); // write the literal '%' byte into the concat-buffer destination cursor - emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after writing the literal percent byte - emitter.instruction("jmp __rt_sprintf_loop_linux_x86_64"); // continue scanning after emitting the literal percent escape - - emitter.label("__rt_sprintf_scan_spec_linux_x86_64"); - emitter.instruction("lea r10, [rbp - 96]"); // point at the mini format-string buffer used for one-specifier snprintf calls - emitter.instruction("mov BYTE PTR [r10], 37"); // seed the mini format-string buffer with the leading '%' introducer - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after writing the leading '%' introducer - - emitter.label("__rt_sprintf_scan_flags_linux_x86_64"); - emitter.instruction("test r13, r13"); // are there any format bytes left to inspect for flag characters? - emitter.instruction("jz __rt_sprintf_end_spec_linux_x86_64"); // bail out cleanly when the format string ends before a type character appears - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the next format byte before deciding whether it is one of the allowed flag characters - emitter.instruction("cmp r8b, 45"); // is the next format byte the left-align '-' flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '-' - emitter.instruction("cmp r8b, 43"); // is the next format byte the explicit plus-sign '+' flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '+' - emitter.instruction("cmp r8b, 48"); // is the next format byte the zero-pad '0' flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '0' - emitter.instruction("cmp r8b, 32"); // is the next format byte the space-sign flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is a space - emitter.instruction("cmp r8b, 35"); // is the next format byte the alternate-form '#' flag? - emitter.instruction("je __rt_sprintf_copy_flag_linux_x86_64"); // copy the current flag byte into the mini format string when it is '#' - emitter.instruction("jmp __rt_sprintf_scan_width_linux_x86_64"); // move on to width parsing once no more flag characters remain - - emitter.label("__rt_sprintf_copy_flag_linux_x86_64"); - emitter.instruction("mov BYTE PTR [r10], r8b"); // append the current flag byte to the mini format string - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending one flag byte - emitter.instruction("add r12, 1"); // consume the current flag byte from the source format string - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming one flag byte - emitter.instruction("jmp __rt_sprintf_scan_flags_linux_x86_64"); // continue scanning for additional flag bytes - - emitter.label("__rt_sprintf_scan_width_linux_x86_64"); - emitter.instruction("test r13, r13"); // are there any format bytes left to inspect for width digits? - emitter.instruction("jz __rt_sprintf_end_spec_linux_x86_64"); // bail out cleanly when the format string ends before a type character appears - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the next format byte before deciding whether it is a width digit - emitter.instruction("cmp r8b, 48"); // is the next format byte below ASCII '0'? - emitter.instruction("jl __rt_sprintf_scan_dot_linux_x86_64"); // move on to precision parsing once the next byte is not a width digit - emitter.instruction("cmp r8b, 57"); // is the next format byte above ASCII '9'? - emitter.instruction("jg __rt_sprintf_scan_dot_linux_x86_64"); // move on to precision parsing once the next byte is not a width digit - emitter.instruction("mov BYTE PTR [r10], r8b"); // append the current width digit to the mini format string - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending one width digit - emitter.instruction("add r12, 1"); // consume the current width digit from the source format string - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming one width digit - emitter.instruction("jmp __rt_sprintf_scan_width_linux_x86_64"); // continue scanning for additional width digits - - emitter.label("__rt_sprintf_scan_dot_linux_x86_64"); - emitter.instruction("cmp r8b, 46"); // is the next format byte the '.' introducer of a precision clause? - emitter.instruction("jne __rt_sprintf_scan_type_linux_x86_64"); // skip precision parsing when the next format byte is not '.' - emitter.instruction("mov BYTE PTR [r10], r8b"); // append the '.' precision introducer to the mini format string - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending the precision introducer - emitter.instruction("add r12, 1"); // consume the precision introducer from the source format string - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming the precision introducer - - emitter.label("__rt_sprintf_scan_prec_linux_x86_64"); - emitter.instruction("test r13, r13"); // are there any format bytes left to inspect for precision digits? - emitter.instruction("jz __rt_sprintf_end_spec_linux_x86_64"); // bail out cleanly when the format string ends before a type character appears - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the next format byte before deciding whether it is a precision digit - emitter.instruction("cmp r8b, 48"); // is the next format byte below ASCII '0'? - emitter.instruction("jl __rt_sprintf_scan_type_linux_x86_64"); // move on to type parsing once the next byte is not a precision digit - emitter.instruction("cmp r8b, 57"); // is the next format byte above ASCII '9'? - emitter.instruction("jg __rt_sprintf_scan_type_linux_x86_64"); // move on to type parsing once the next byte is not a precision digit - emitter.instruction("mov BYTE PTR [r10], r8b"); // append the current precision digit to the mini format string - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending one precision digit - emitter.instruction("add r12, 1"); // consume the current precision digit from the source format string - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming one precision digit - emitter.instruction("jmp __rt_sprintf_scan_prec_linux_x86_64"); // continue scanning for additional precision digits - - emitter.label("__rt_sprintf_scan_type_linux_x86_64"); - emitter.instruction("test r13, r13"); // is the format string exhausted before a terminal type character appears? - emitter.instruction("jz __rt_sprintf_end_spec_linux_x86_64"); // bail out cleanly when the format string ends before the type character - emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the terminal type character that completes the current mini format string - emitter.instruction("add r12, 1"); // consume the type character from the source format string - emitter.instruction("sub r13, 1"); // decrement the remaining format length after consuming the type character - emitter.instruction("mov BYTE PTR [r10], r8b"); // append the terminal type character to the mini format string - emitter.instruction("add r10, 1"); // advance the mini format-string cursor after appending the type character - emitter.instruction("mov BYTE PTR [r10], 0"); // null-terminate the one-specifier mini format string for the upcoming snprintf call - emitter.instruction("mov r9, r14"); // copy the packed variadic argument index before scaling it into a caller-stack record offset - emitter.instruction("shl r9, 4"); // convert the packed variadic argument index into the byte offset of the current 16-byte caller-stack record - emitter.instruction("lea r9, [r15 + r9]"); // compute the address of the current packed variadic argument record on the caller stack - emitter.instruction("add r14, 1"); // consume one packed variadic argument record for the current format specifier - emitter.instruction("cmp r8b, 102"); // is the terminal type character '%f'? - emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%f' - emitter.instruction("cmp r8b, 101"); // is the terminal type character '%e'? - emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%e' - emitter.instruction("cmp r8b, 103"); // is the terminal type character '%g'? - emitter.instruction("je __rt_sprintf_call_float_linux_x86_64"); // dispatch to the float snprintf path when the terminal type character is '%g' - emitter.instruction("cmp r8b, 115"); // is the terminal type character '%s'? - emitter.instruction("je __rt_sprintf_call_string_linux_x86_64"); // dispatch to the string snprintf path when the terminal type character is '%s' - emitter.instruction("cmp r8b, 99"); // is the terminal type character '%c'? - emitter.instruction("je __rt_sprintf_call_char_linux_x86_64"); // dispatch to the char snprintf path when the terminal type character is '%c' - emitter.instruction("jmp __rt_sprintf_call_int_linux_x86_64"); // treat all remaining supported type characters as integer-like snprintf operands - - emitter.label("__rt_sprintf_end_spec_linux_x86_64"); - emitter.instruction("jmp __rt_sprintf_done_linux_x86_64"); // stop formatting when the format string ends partway through a specifier - - emitter.label("__rt_sprintf_call_float_linux_x86_64"); - emitter.instruction("movq xmm0, QWORD PTR [r9]"); // load the packed floating-point bits into xmm0 for the SysV variadic snprintf call - emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer - emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes - emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov eax, 1"); // advertise one live SIMD variadic register to the SysV variadic call ABI - emitter.bl_c("snprintf"); // format the floating operand into the local snprintf output scratch buffer - emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer - - emitter.label("__rt_sprintf_call_int_linux_x86_64"); - emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer - emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes - emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov rcx, QWORD PTR [r9]"); // load the packed integer payload into the first SysV variadic integer register - emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the integer snprintf call - emitter.bl_c("snprintf"); // format the integer-like operand into the local snprintf output scratch buffer - emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer - - emitter.label("__rt_sprintf_call_char_linux_x86_64"); - emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer - emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes - emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov ecx, DWORD PTR [r9]"); // load the packed character payload into the first SysV variadic integer register - emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the char snprintf call - emitter.bl_c("snprintf"); // format the character operand into the local snprintf output scratch buffer - emitter.instruction("jmp __rt_sprintf_copy_result_linux_x86_64"); // copy the freshly formatted snprintf output into the concat buffer - - emitter.label("__rt_sprintf_call_string_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [r9]"); // load the packed elephc string pointer before converting it into a null-terminated C string - emitter.instruction("mov rdx, QWORD PTR [r9 + 8]"); // load the packed string metadata word before extracting the elephc string length - emitter.instruction("shr rdx, 8"); // extract the original elephc string length from the packed string metadata word - emitter.instruction("call __rt_cstr"); // convert the elephc string pointer+length pair into a null-terminated C string in the scratch buffer - emitter.instruction("lea rdi, [rbp - 224]"); // point snprintf at the fixed local output scratch buffer - emitter.instruction("mov esi, 128"); // bound the local snprintf output scratch buffer to 128 bytes - emitter.instruction("lea rdx, [rbp - 96]"); // pass the one-specifier mini format string to snprintf as the format pointer - emitter.instruction("mov rcx, rax"); // pass the null-terminated C string pointer in the first SysV variadic integer register - emitter.instruction("xor eax, eax"); // advertise that no SIMD variadic registers are live for the string snprintf call - emitter.bl_c("snprintf"); // format the string operand into the local snprintf output scratch buffer - - emitter.label("__rt_sprintf_copy_result_linux_x86_64"); - emitter.instruction("mov rcx, rax"); // copy the snprintf-written byte count before the result-copy loop consumes caller-saved registers - emitter.instruction("lea r10, [rbp - 224]"); // point at the local snprintf output scratch buffer before copying its bytes into the concat buffer - emitter.label("__rt_sprintf_copy_loop_linux_x86_64"); - emitter.instruction("test rcx, rcx"); // have all bytes produced by snprintf already been copied into the concat buffer? - emitter.instruction("jz __rt_sprintf_loop_linux_x86_64"); // resume scanning the source format string once the local snprintf output has been fully copied - emitter.instruction("mov r11b, BYTE PTR [r10]"); // load the next byte from the local snprintf output scratch buffer - emitter.instruction("mov BYTE PTR [rbx], r11b"); // store the current snprintf output byte into the concat-buffer destination cursor - emitter.instruction("add r10, 1"); // advance the local snprintf output cursor after copying one byte - emitter.instruction("add rbx, 1"); // advance the concat-buffer destination cursor after copying one byte - emitter.instruction("sub rcx, 1"); // decrement the remaining snprintf output byte count after copying one byte - emitter.instruction("jmp __rt_sprintf_copy_loop_linux_x86_64"); // continue copying snprintf output until every byte has been appended to the concat buffer - - emitter.label("__rt_sprintf_done_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // return the concat-buffer start pointer of the formatted string in the primary x86_64 string result register - emitter.instruction("mov rdx, rbx"); // copy the concat-buffer end cursor so the final formatted-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the formatted-string length from the concat-buffer start/end pointers - emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the concat-offset symbol address before publishing the new write cursor - emitter.instruction("mov r11, QWORD PTR [r10]"); // reload the old concat-buffer write cursor before advancing it by the formatted-string length - emitter.instruction("add r11, rdx"); // advance the concat-buffer write cursor by the emitted formatted-string length - emitter.instruction("mov QWORD PTR [r10], r11"); // publish the updated concat-buffer write cursor after emitting the formatted string - emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the packed variadic argument count before discarding the caller-owned tagged argument records - emitter.instruction("shl rcx, 4"); // convert the packed variadic argument count into the total tagged-record byte count on the caller stack - emitter.instruction("add rsp, 328"); // release the local sprintf() buffers before restoring callee-saved registers - emitter.instruction("pop r15"); // restore the caller packed-argument base-pointer callee-saved register - emitter.instruction("pop r14"); // restore the caller packed-argument index callee-saved register - emitter.instruction("pop r13"); // restore the caller format-length callee-saved register - emitter.instruction("pop r12"); // restore the caller format-pointer callee-saved register - emitter.instruction("pop rbx"); // restore the caller concat-destination callee-saved register - emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // preserve the dynamic return address before rethreading the stack past the caller-owned tagged argument records - emitter.instruction("mov rbp, QWORD PTR [rsp]"); // restore the caller frame pointer without consuming the current stack slot yet - emitter.instruction("lea rsp, [rsp + rcx + 16]"); // advance past the saved frame pointer, return address, and tagged variadic records to the caller post-call stack top - emitter.instruction("push r11"); // recreate the preserved return address at the top of the rethreaded stack so a plain ret lands back in generated code - emitter.instruction("ret"); // return the formatted string in the standard x86_64 string result registers while also discarding the caller-owned tagged arguments + // Frame layout, relative to rbp: + // [rbp-8 .. rbp-40] = pushed rbx, r12, r13, r14, r15 + // [rbp-48] = result start pointer inside _concat_buf + // [rbp-56] = address of the _concat_off symbol + // [rbp-64] = packed argument record count + // [rbp-72] = parsed field width + // [rbp-80] = parsed precision (-1 when the specifier had no '.') + // [rbp-88] = parsed flags: bit0 left-align, bit1 force sign, bit2 alt form + // [rbp-96] = parsed pad character + // [rbp-104] = parsed conversion character + // [rbp-112] = parsed argument number (0 = next sequential argument) + // [rbp-120] = one-past-the-end address of _concat_buf + // [rbp-160 .. rbp-129] = mini C format string built by this helper + // [rbp-672 .. rbp-161] = snprintf conversion scratch (CONV_SCRATCH_CAP bytes) + + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for every local slot + emitter.instruction("push rbx"); // preserve the concat-buffer write cursor register + emitter.instruction("push r12"); // preserve the format-cursor register + emitter.instruction("push r13"); // preserve the remaining-format-length register + emitter.instruction("push r14"); // preserve the sequential-argument-index register + emitter.instruction("push r15"); // preserve the argument-record base register + emitter.instruction("sub rsp, 632"); // reserve the parse slots, mini format buffer and conversion scratch + emitter.instruction("mov r12, rax"); // format cursor + emitter.instruction("mov r13, rdx"); // remaining format bytes + emitter.instruction("xor r14d, r14d"); // next sequential argument index + emitter.instruction("lea r15, [rbp + 16]"); // argument records begin above the saved return address + emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // remember how many records the caller pushed + abi::emit_symbol_address(emitter, "r10", "_concat_off"); + emitter.instruction("mov r11, QWORD PTR [r10]"); // current concat-buffer write offset + abi::emit_symbol_address(emitter, "rcx", "_concat_buf"); + emitter.instruction("lea rbx, [rcx + r11]"); // write cursor = buffer base + offset + emitter.instruction("mov QWORD PTR [rbp - 48], rbx"); // remember where this result starts + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // remember the concat-offset symbol address + emitter.instruction(&format!("lea rcx, [rcx + {}]", CONCAT_BUF_CAP)); // one-past-the-end address of the concat buffer + emitter.instruction("mov QWORD PTR [rbp - 120], rcx"); // publish the hard write limit for every copy below + + // ================================================================ + // MAIN SCAN LOOP: literal bytes are copied, '%' starts a specifier + // ================================================================ + emitter.label("__rt_sprintf_loop_x64"); + emitter.instruction("test r13, r13"); // any format bytes left? + emitter.instruction("jz __rt_sprintf_done_x64"); // no → publish the result + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the next format byte + emitter.instruction("add r12, 1"); // advance the format cursor + emitter.instruction("sub r13, 1"); // account for the consumed format byte + emitter.instruction("cmp r8b, 37"); // is it '%'? + emitter.instruction("je __rt_sprintf_fmt_x64"); // yes → parse a conversion specifier + emitter.instruction("mov r9, QWORD PTR [rbp - 120]"); // reload the concat-buffer write limit + emitter.instruction("cmp rbx, r9"); // would this literal byte land outside the arena? + emitter.instruction("jae __rt_sprintf_ofatal_x64"); // yes → controlled fatal instead of an overrun + emitter.instruction("mov BYTE PTR [rbx], r8b"); // copy the literal byte to the result + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("jmp __rt_sprintf_loop_x64"); // continue scanning + + emitter.label("__rt_sprintf_fmt_x64"); + emitter.instruction("test r13, r13"); // trailing '%' with nothing after it? + emitter.instruction("jz __rt_sprintf_done_x64"); // yes → publish the result + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the byte after '%' + emitter.instruction("cmp r8b, 37"); // is the sequence '%%'? + emitter.instruction("jne __rt_sprintf_spec_x64"); // no → parse a real specifier + emitter.instruction("add r12, 1"); // consume the second '%' + emitter.instruction("sub r13, 1"); // account for the consumed byte + emitter.instruction("mov r9, QWORD PTR [rbp - 120]"); // reload the concat-buffer write limit + emitter.instruction("cmp rbx, r9"); // would the literal '%' land outside the arena? + emitter.instruction("jae __rt_sprintf_ofatal_x64"); // yes → controlled fatal instead of an overrun + emitter.instruction("mov BYTE PTR [rbx], r8b"); // emit the literal '%' + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("jmp __rt_sprintf_loop_x64"); // continue scanning + + emit_spec_parser(emitter); + emit_argument_fetch(emitter); + emit_conversion_dispatch(emitter); + emit_string_conversion(emitter); + emit_binary_conversion(emitter); + emit_char_conversion(emitter); + emit_integer_conversion(emitter); + emit_float_conversion(emitter); + emit_snprintf_result(emitter); + emit_exponent_compaction(emitter); + emit_pad_and_copy(emitter); + + // ================================================================ + // DONE: publish the result and discard the caller's argument records + // ================================================================ + emitter.label("__rt_sprintf_done_x64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // result pointer inside the concat buffer + emitter.instruction("mov rdx, rbx"); // current write cursor + emitter.instruction("sub rdx, rax"); // result byte length + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // concat-offset symbol address + emitter.instruction("mov r11, QWORD PTR [r10]"); // current concat-buffer write offset + emitter.instruction("add r11, rdx"); // advance it past this result + emitter.instruction("mov QWORD PTR [r10], r11"); // publish the new write offset + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // packed argument record count + emitter.instruction("shl rcx, 4"); // records are 16 bytes each + emitter.instruction("add rsp, 632"); // release the local buffers + emitter.instruction("pop r15"); // restore the argument-record base register + emitter.instruction("pop r14"); // restore the sequential-argument-index register + emitter.instruction("pop r13"); // restore the remaining-format-length register + emitter.instruction("pop r12"); // restore the format-cursor register + emitter.instruction("pop rbx"); // restore the concat-buffer write cursor register + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // save the return address before rethreading the stack + emitter.instruction("mov rbp, QWORD PTR [rsp]"); // restore the caller frame pointer + emitter.instruction("lea rsp, [rsp + rcx + 16]"); // skip the saved rbp, return address and tagged records + emitter.instruction("push r11"); // recreate the return address on the rethreaded stack + emitter.instruction("ret"); // return the formatted string in rax/rdx + + emit_fatal_paths(emitter); +} + +/// Emits an x86_64 decimal-number scanner used for the argument number, the field width, +/// and the precision. +/// +/// `ptr`/`len` are the source cursor and remaining-byte count; both are advanced past the +/// digits consumed. `r10` receives the value and `r11` the digit count; both must be zeroed +/// by the caller. `r8b` is left holding the first non-digit byte, or zero when the input ran +/// out, so the caller can tell "stopped on `$`" from "ran out of format". `r9` is clobbered. +/// +/// Accumulation stops after 10 digits and any longer run saturates to `0x80000000`, which +/// keeps the accumulator inside 64 bits and makes "wider than `INT_MAX`" detectable as +/// `value >> 31 != 0` no matter how many digits the program supplied. +fn emit_scan_decimal(emitter: &mut Emitter, prefix: &str, ptr: &str, len: &str) { + emitter.label(&format!("{}_loop", prefix)); + emitter.instruction(&format!("test {0}, {0}", len)); // any bytes left to inspect? + emitter.instruction(&format!("jz {}_end0", prefix)); // no → the number ends here + emitter.instruction(&format!("movzx r8d, BYTE PTR [{}]", ptr)); // peek at the current byte + emitter.instruction("mov r9d, r8d"); // copy it before converting to a digit value + emitter.instruction("sub r9d, 48"); // convert the byte to a digit value + emitter.instruction("cmp r9d, 9"); // is it outside '0'..'9'? + emitter.instruction(&format!("ja {}_done", prefix)); // yes → the number ends here + emitter.instruction("cmp r11, 10"); // already accumulated ten digits? + emitter.instruction(&format!("jae {}_skip", prefix)); // yes → stop accumulating, just count + emitter.instruction("lea r10, [r10 + r10*4]"); // accumulator *= 5 + emitter.instruction("add r10, r10"); // accumulator *= 2, so *= 10 overall + emitter.instruction("add r10, r9"); // add the current digit + emitter.label(&format!("{}_skip", prefix)); + emitter.instruction("add r11, 1"); // count the consumed digit + emitter.instruction(&format!("add {}, 1", ptr)); // advance the source cursor + emitter.instruction(&format!("sub {}, 1", len)); // account for the consumed byte + emitter.instruction(&format!("jmp {}_loop", prefix)); // scan the next digit + emitter.label(&format!("{}_end0", prefix)); + emitter.instruction("xor r8d, r8d"); // no lookahead byte is available + emitter.label(&format!("{}_done", prefix)); + emitter.instruction("cmp r11, 10"); // did the run exceed ten digits? + emitter.instruction(&format!("jbe {}_nosat", prefix)); // no → keep the accumulated value + emitter.instruction("mov r10d, 2147483648"); // saturate above INT_MAX so the range check fires + emitter.label(&format!("{}_nosat", prefix)); +} + +/// Emits the x86_64 specifier parser: argument number, flags, pad character, width and +/// precision are decoded into frame slots and the conversion character is stored last. +/// +/// Nothing here copies program-supplied bytes into a buffer, so an arbitrarily long +/// specifier costs scan time only — it can never overrun the mini format buffer. +fn emit_spec_parser(emitter: &mut Emitter) { + // -- reset the per-specifier state -- + emitter.label("__rt_sprintf_spec_x64"); + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // width = 0 + emitter.instruction("mov QWORD PTR [rbp - 80], -1"); // precision = absent + emitter.instruction("mov QWORD PTR [rbp - 88], 0"); // flags = none + emitter.instruction("mov QWORD PTR [rbp - 96], 32"); // pad character = ' ' + emitter.instruction("mov QWORD PTR [rbp - 112], 0"); // argument number = sequential + + // -- optional "N$" argument number: only committed when a '$' follows the digits -- + emitter.instruction("mov rsi, r12"); // lookahead cursor (does not consume yet) + emitter.instruction("mov rdi, r13"); // lookahead remaining-byte count + emitter.instruction("xor r10d, r10d"); // argument-number accumulator + emitter.instruction("xor r11d, r11d"); // argument-number digit count + emit_scan_decimal(emitter, "__rt_sprintf_an_x64", "rsi", "rdi"); + emitter.instruction("test r11, r11"); // were there any digits? + emitter.instruction("jz __rt_sprintf_flags_x64"); // no → not an argument number + emitter.instruction("cmp r8b, 36"); // is the byte after the digits '$'? + emitter.instruction("jne __rt_sprintf_flags_x64"); // no → those digits are the field width + emitter.instruction("mov QWORD PTR [rbp - 112], r10"); // commit the explicit argument number + emitter.instruction("lea r12, [rsi + 1]"); // consume the digits and the '$' + emitter.instruction("lea r13, [rdi - 1]"); // account for the consumed '$' + + // -- flags: '-', '+', '0', ' ', '#', and PHP's "'X" custom pad character -- + emitter.label("__rt_sprintf_flags_x64"); + emitter.instruction("test r13, r13"); // format ended inside the specifier? + emitter.instruction("jz __rt_sprintf_endspec_x64"); // yes → stop formatting + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the current specifier byte + emitter.instruction("cmp r8b, 45"); // '-' left-align flag? + emitter.instruction("je __rt_sprintf_fl_left_x64"); // yes → record left alignment + emitter.instruction("cmp r8b, 43"); // '+' force-sign flag? + emitter.instruction("je __rt_sprintf_fl_plus_x64"); // yes → record the forced sign + emitter.instruction("cmp r8b, 48"); // '0' zero-pad flag? + emitter.instruction("je __rt_sprintf_fl_zero_x64"); // yes → pad character becomes '0' + emitter.instruction("cmp r8b, 32"); // ' ' space-pad flag? + emitter.instruction("je __rt_sprintf_fl_space_x64"); // yes → pad character becomes ' ' + emitter.instruction("cmp r8b, 35"); // '#' alternate-form flag? + emitter.instruction("je __rt_sprintf_fl_alt_x64"); // yes → record the alternate form + emitter.instruction("cmp r8b, 39"); // "'" custom-pad-character flag? + emitter.instruction("je __rt_sprintf_fl_pad_x64"); // yes → the next byte is the pad character + emitter.instruction("jmp __rt_sprintf_width_x64"); // no more flags → parse the width + + emitter.label("__rt_sprintf_fl_left_x64"); + emitter.instruction("or QWORD PTR [rbp - 88], 1"); // set the left-align flag bit + emitter.instruction("jmp __rt_sprintf_fl_next_x64"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_plus_x64"); + emitter.instruction("or QWORD PTR [rbp - 88], 2"); // set the force-sign flag bit + emitter.instruction("jmp __rt_sprintf_fl_next_x64"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_alt_x64"); + emitter.instruction("or QWORD PTR [rbp - 88], 4"); // set the alternate-form flag bit + emitter.instruction("jmp __rt_sprintf_fl_next_x64"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_zero_x64"); + emitter.instruction("mov QWORD PTR [rbp - 96], 48"); // '0' becomes the pad character + emitter.instruction("jmp __rt_sprintf_fl_next_x64"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_space_x64"); + emitter.instruction("mov QWORD PTR [rbp - 96], 32"); // ' ' becomes the pad character + emitter.instruction("jmp __rt_sprintf_fl_next_x64"); // consume the flag byte + + emitter.label("__rt_sprintf_fl_pad_x64"); + emitter.instruction("add r12, 1"); // consume the "'" introducer + emitter.instruction("sub r13, 1"); // account for the consumed byte + emitter.instruction("test r13, r13"); // "'" at end of format? + emitter.instruction("jz __rt_sprintf_endspec_x64"); // yes → nothing to pad with + emitter.instruction("movzx r9d, BYTE PTR [r12]"); // the next byte is the custom pad character + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // store the custom pad character + + emitter.label("__rt_sprintf_fl_next_x64"); + emitter.instruction("add r12, 1"); // consume the flag byte + emitter.instruction("sub r13, 1"); // account for the consumed byte + emitter.instruction("jmp __rt_sprintf_flags_x64"); // look for another flag + + // -- field width -- + emitter.label("__rt_sprintf_width_x64"); + emitter.instruction("xor r10d, r10d"); // width accumulator + emitter.instruction("xor r11d, r11d"); // width digit count + emit_scan_decimal(emitter, "__rt_sprintf_w_x64", "r12", "r13"); + emitter.instruction("mov QWORD PTR [rbp - 72], r10"); // store the parsed field width + + // -- optional ".precision" -- + emitter.instruction("test r13, r13"); // format ended before the conversion? + emitter.instruction("jz __rt_sprintf_endspec_x64"); // yes → stop formatting + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // peek at the current specifier byte + emitter.instruction("cmp r8b, 46"); // '.' precision introducer? + emitter.instruction("jne __rt_sprintf_stype_x64"); // no → the conversion character follows + emitter.instruction("add r12, 1"); // consume the '.' + emitter.instruction("sub r13, 1"); // account for the consumed byte + emitter.instruction("xor r10d, r10d"); // precision accumulator ('.' alone means 0) + emitter.instruction("xor r11d, r11d"); // precision digit count + emit_scan_decimal(emitter, "__rt_sprintf_p_x64", "r12", "r13"); + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // store the parsed precision + + // -- conversion character -- + emitter.label("__rt_sprintf_stype_x64"); + emitter.instruction("test r13, r13"); // format ended before the conversion? + emitter.instruction("jz __rt_sprintf_endspec_x64"); // yes → stop formatting + emitter.instruction("movzx r8d, BYTE PTR [r12]"); // load the conversion character + emitter.instruction("add r12, 1"); // consume the conversion character + emitter.instruction("sub r13, 1"); // account for the consumed byte + emitter.instruction("mov QWORD PTR [rbp - 104], r8"); // store the conversion character + emitter.instruction("jmp __rt_sprintf_arg_x64"); // fetch the argument this conversion consumes + + emitter.label("__rt_sprintf_endspec_x64"); + emitter.instruction("jmp __rt_sprintf_done_x64"); // truncated specifier → stop formatting +} + +/// Emits the x86_64 argument fetch: resolves the sequential or explicit `N$` argument index, +/// rejects out-of-range indices, and loads the 16-byte record into `r10`/`r11`. +/// +/// The range check is what keeps the helper from reading the caller's stack past the pushed +/// records when a format string requests more arguments than were supplied. +fn emit_argument_fetch(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_arg_x64"); + emitter.instruction("mov r9, QWORD PTR [rbp - 112]"); // parsed argument number (0 = sequential) + emitter.instruction("test r9, r9"); // was an explicit number given? + emitter.instruction("jz __rt_sprintf_arg_seq_x64"); // no → take the next argument + emitter.instruction("sub r9, 1"); // PHP argument numbers are 1-based + emitter.instruction("jmp __rt_sprintf_arg_have_x64"); // index resolved + emitter.label("__rt_sprintf_arg_seq_x64"); + emitter.instruction("mov r9, r14"); // consume the next sequential argument + emitter.instruction("add r14, 1"); // advance the sequential cursor + emitter.label("__rt_sprintf_arg_have_x64"); + emitter.instruction("cmp r9, QWORD PTR [rbp - 64]"); // is the index within the supplied records? + emitter.instruction("jae __rt_sprintf_afatal_x64"); // no → controlled fatal instead of a stack read + emitter.instruction("shl r9, 4"); // records are 16 bytes each + emitter.instruction("lea r9, [r15 + r9]"); // address of the selected record + emitter.instruction("mov r10, QWORD PTR [r9]"); // record payload word + emitter.instruction("mov r11, QWORD PTR [r9 + 8]"); // record tag word (tag | length << 8) + emitter.instruction("movzx r8d, BYTE PTR [rbp - 104]"); // reload the conversion character +} + +/// Emits the x86_64 conversion dispatch. Only the conversion characters PHP defines are +/// accepted; anything else takes the controlled `ValueError` path rather than being handed +/// to libc, which is what keeps `%n` and other libc-only conversions unreachable. +fn emit_conversion_dispatch(emitter: &mut Emitter) { + emitter.instruction("cmp r8b, 115"); // 's' string conversion? + emitter.instruction("je __rt_sprintf_t_str_x64"); // yes → string path + emitter.instruction("cmp r8b, 100"); // 'd' signed decimal? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer path + emitter.instruction("cmp r8b, 117"); // 'u' unsigned decimal? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer path + emitter.instruction("cmp r8b, 111"); // 'o' octal? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer path + emitter.instruction("cmp r8b, 120"); // 'x' lowercase hexadecimal? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer path + emitter.instruction("cmp r8b, 88"); // 'X' uppercase hexadecimal? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer path + emitter.instruction("cmp r8b, 98"); // 'b' binary? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer coercion, then the binary body + emitter.instruction("cmp r8b, 99"); // 'c' single character? + emitter.instruction("je __rt_sprintf_t_int_x64"); // yes → integer coercion, then the single-byte body + emitter.instruction("cmp r8b, 102"); // 'f' fixed-point? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("cmp r8b, 70"); // 'F' locale-independent fixed-point? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("cmp r8b, 101"); // 'e' scientific? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("cmp r8b, 69"); // 'E' uppercase scientific? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("cmp r8b, 103"); // 'g' shortest-of-e-or-f? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("cmp r8b, 71"); // 'G' uppercase shortest-of-E-or-f? + emitter.instruction("je __rt_sprintf_t_flt_x64"); // yes → float path + emitter.instruction("jmp __rt_sprintf_sfatal_x64"); // PHP rejects every other conversion +} + +/// Emits the x86_64 `%s` conversion. +/// +/// A string record is emitted straight from its pointer/length pair (so the result is +/// binary safe and not capped at any scratch-buffer size); precision truncates it. A record +/// carrying another tag is rendered numerically instead of being dereferenced. +fn emit_string_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_str_x64"); + emitter.instruction("mov rax, r11"); // copy the record tag word + emitter.instruction("and rax, 255"); // isolate the record type tag + emitter.instruction("cmp rax, 1"); // is this record actually a string? + emitter.instruction("jne __rt_sprintf_str_num_x64"); // no → render the payload as a number + emitter.instruction("shr r11, 8"); // string byte length lives above the tag + emitter.instruction("test r10, r10"); // is the string pointer null? + emitter.instruction("jnz __rt_sprintf_str_ptr_x64"); // no → the pointer carries bytes + emitter.instruction("xor r11d, r11d"); // treat a null string pointer as empty + emitter.label("__rt_sprintf_str_ptr_x64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // parsed precision + emitter.instruction("test rax, rax"); // was a precision given? + emitter.instruction("js __rt_sprintf_emit_x64"); // no → emit the whole string + emitter.instruction("cmp r11, rax"); // is the string within the precision? + emitter.instruction("jbe __rt_sprintf_emit_x64"); // yes → emit it unchanged + emitter.instruction("mov r11, rax"); // truncate the string to the precision + emitter.instruction("jmp __rt_sprintf_emit_x64"); // pad and copy the string body + + // -- non-string record under %s: format the payload instead of dereferencing it -- + emitter.label("__rt_sprintf_str_num_x64"); + emitter.instruction("mov QWORD PTR [rbp - 80], -1"); // the %s precision must not reach the numeric path + emitter.instruction("cmp rax, 2"); // is the payload a double? + emitter.instruction("jne __rt_sprintf_str_int_x64"); // no → render it as a signed integer + emitter.instruction("mov QWORD PTR [rbp - 80], 14"); // PHP renders floats with 14 significant digits + emitter.instruction("mov r8d, 71"); // reuse the 'G' float conversion + emitter.instruction("mov QWORD PTR [rbp - 104], r8"); // record the substituted conversion character + emitter.instruction("jmp __rt_sprintf_t_flt_x64"); // format through the float path + emitter.label("__rt_sprintf_str_int_x64"); + emitter.instruction("mov r8d, 100"); // reuse the 'd' integer conversion + emitter.instruction("mov QWORD PTR [rbp - 104], r8"); // record the substituted conversion character + emitter.instruction("jmp __rt_sprintf_t_int_x64"); // format through the integer path +} + +/// Emits the x86_64 `%b` conversion body, which libc has no portable equivalent for. +/// Entered from the shared integer coercion with the operand already in `r10`. Digits are +/// generated backwards into the conversion scratch, so at most 64 bytes are written and the +/// result never carries leading zeros (PHP prints `0` for zero). +fn emit_binary_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_bin_go_x64"); + emitter.instruction("lea r9, [rbp - 600]"); // write backwards from conversion scratch + 72 bytes + emitter.instruction("xor r11d, r11d"); // generated digit count + emitter.label("__rt_sprintf_bin_loop_x64"); + emitter.instruction("mov rax, r10"); // copy the remaining value + emitter.instruction("and rax, 1"); // take its low bit + emitter.instruction("add al, 48"); // turn the bit into an ASCII digit + emitter.instruction("sub r9, 1"); // step one byte back in the scratch + emitter.instruction("mov BYTE PTR [r9], al"); // store the digit + emitter.instruction("add r11, 1"); // count the digit + emitter.instruction("shr r10, 1"); // shift the value right by one bit + emitter.instruction("jnz __rt_sprintf_bin_loop_x64"); // more bits → keep going + emitter.instruction("mov r10, r9"); // body pointer = first generated digit + emitter.instruction("jmp __rt_sprintf_emit_x64"); // pad and copy the binary body +} + +/// Emits the x86_64 `%c` conversion body, entered from the shared integer coercion with the +/// operand already in `r10`. PHP appends the low byte of the argument and ignores width and +/// padding entirely, so the width slot is cleared before emitting. +fn emit_char_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_chr_go_x64"); + emitter.instruction("lea r9, [rbp - 672]"); // reuse the conversion scratch for one byte + emitter.instruction("mov BYTE PTR [r9], r10b"); // store the low byte of the argument + emitter.instruction("mov r10, r9"); // body pointer = the stored byte + emitter.instruction("mov r11d, 1"); // body length = one byte + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // PHP ignores width for %c + emitter.instruction("jmp __rt_sprintf_emit_x64"); // copy the single byte +} + +/// Emits the x86_64 integer conversions (`%d`, `%u`, `%o`, `%x`, `%X`) plus the shared +/// operand coercion that `%b` and `%c` also enter through. +/// +/// The record tag decides the coercion: a double is truncated toward zero and a string is +/// parsed by `__rt_str_to_int`. Without that string case the helper would print the operand +/// pointer whenever the conversion character and the packed record disagree — which happens +/// for `v*printf()`, for a runtime-built format string, and for `%1$s`/`%1$d` on one +/// argument. The length handed to `__rt_str_to_int` is clamped to the C-string scratch. +/// +/// The C format string is assembled from the parsed flags rather than copied from the +/// program, so it is at most `"%+#llX"` plus a NUL. Precision is deliberately omitted: +/// PHP ignores it for integer conversions. +fn emit_integer_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_int_x64"); + emitter.instruction("mov rax, r11"); // copy the record tag word + emitter.instruction("and rax, 255"); // isolate the record type tag + emitter.instruction("cmp rax, 1"); // is the payload a string pointer? + emitter.instruction("je __rt_sprintf_int_str_x64"); // yes → parse it instead of printing the pointer + emitter.instruction("cmp rax, 2"); // is the payload a double? + emitter.instruction("jne __rt_sprintf_int_ready_x64"); // no → the payload is already an integer + emitter.instruction("movq xmm0, r10"); // move the double bits into an SSE register + emitter.instruction("cvttsd2si r10, xmm0"); // truncate the double toward zero like PHP + emitter.instruction("jmp __rt_sprintf_int_ready_x64"); // the operand is an integer now + emitter.label("__rt_sprintf_int_str_x64"); + emitter.instruction("mov rax, r10"); // string pointer for the numeric parse + emitter.instruction("mov rdx, r11"); // record tag word holding the string length + emitter.instruction("shr rdx, 8"); // string byte length for the numeric parse + emitter.instruction("test rax, rax"); // is the string pointer null? + emitter.instruction("jz __rt_sprintf_int_str_null_x64"); // yes → a null pointer parses as zero + emitter.instruction("cmp rdx, 4095"); // __rt_cstr copies into a 4096-byte scratch + emitter.instruction("jbe __rt_sprintf_int_str_go_x64"); // the string already fits the C-string scratch + emitter.instruction("mov edx, 4095"); // clamp so the numeric prefix parse stays in bounds + emitter.label("__rt_sprintf_int_str_go_x64"); + emitter.instruction("call __rt_str_to_int"); // PHP leading-numeric string-to-int conversion + emitter.instruction("mov r10, rax"); // the parsed integer becomes the operand + emitter.instruction("jmp __rt_sprintf_int_ready_x64"); // the operand is an integer now + emitter.label("__rt_sprintf_int_str_null_x64"); + emitter.instruction("xor r10d, r10d"); // a null string operand formats as zero + emitter.label("__rt_sprintf_int_ready_x64"); + emitter.instruction("movzx r8d, BYTE PTR [rbp - 104]"); // reload the conversion character after the parse + emitter.instruction("cmp r8b, 98"); // is this the binary conversion? + emitter.instruction("je __rt_sprintf_bin_go_x64"); // yes → generate binary digits by hand + emitter.instruction("cmp r8b, 99"); // is this the single-character conversion? + emitter.instruction("je __rt_sprintf_chr_go_x64"); // yes → emit the low byte directly + emitter.label("__rt_sprintf_int_go_x64"); + emitter.instruction("lea r9, [rbp - 160]"); // mini C format cursor + emitter.instruction("mov BYTE PTR [r9], 37"); // write the '%' introducer + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("cmp r8b, 100"); // only 'd' is signed, so only it can force a sign + emitter.instruction("jne __rt_sprintf_int_noplus_x64"); // other integer conversions ignore '+' + emitter.instruction("test QWORD PTR [rbp - 88], 2"); // is the force-sign flag set? + emitter.instruction("jz __rt_sprintf_int_noplus_x64"); // no → skip the '+' flag + emitter.instruction("mov BYTE PTR [r9], 43"); // write the '+' flag + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.label("__rt_sprintf_int_noplus_x64"); + emitter.instruction("cmp r8b, 100"); // '#' is meaningless for 'd' + emitter.instruction("je __rt_sprintf_int_noalt_x64"); // skip the alternate-form flag + emitter.instruction("cmp r8b, 117"); // '#' is meaningless for 'u' + emitter.instruction("je __rt_sprintf_int_noalt_x64"); // skip the alternate-form flag + emitter.instruction("test QWORD PTR [rbp - 88], 4"); // is the alternate-form flag set? + emitter.instruction("jz __rt_sprintf_int_noalt_x64"); // no → skip the '#' flag + emitter.instruction("mov BYTE PTR [r9], 35"); // write the '#' flag + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.label("__rt_sprintf_int_noalt_x64"); + emitter.instruction("mov BYTE PTR [r9], 108"); // write the first 'l' length modifier + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("mov BYTE PTR [r9], 108"); // write the second 'l' for a 64-bit operand + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("mov BYTE PTR [r9], r8b"); // write the conversion character + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("mov BYTE PTR [r9], 0"); // NUL-terminate the mini C format string + emitter.instruction("lea rdi, [rbp - 672]"); // conversion scratch destination + emitter.instruction(&format!("mov esi, {}", CONV_SCRATCH_CAP)); // conversion scratch capacity + emitter.instruction("lea rdx, [rbp - 160]"); // the mini C format string + emitter.instruction("mov rcx, r10"); // the integer operand as the first variadic + emitter.instruction("xor eax, eax"); // no SSE variadic registers are live + emitter.bl_c("snprintf"); // render the integer body through libc + emitter.instruction("jmp __rt_sprintf_snret_x64"); // clamp and take the result +} + +/// Emits the x86_64 float conversions (`%f`, `%F`, `%e`, `%E`, `%g`, `%G`). +/// +/// The record tag decides the coercion: an int/bool payload is widened and a string is +/// parsed by `__rt_str_to_number`, so a mismatched record never reaches libc as raw pointer +/// bits. Precision is clamped to PHP's 53-digit maximum, which is what bounds the libc output to +/// the conversion scratch. `%f`/`%F`/`%e`/`%E` of negative zero print unsigned in PHP (its +/// own float renderer never emits the sign), while `%g`/`%G` keep it. +fn emit_float_conversion(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_t_flt_x64"); + emitter.instruction("mov rax, r11"); // copy the record tag word + emitter.instruction("and rax, 255"); // isolate the record type tag + emitter.instruction("cmp rax, 2"); // is the payload already a double? + emitter.instruction("je __rt_sprintf_flt_bits_x64"); // yes → use its bit pattern directly + emitter.instruction("cmp rax, 1"); // is the payload a string pointer? + emitter.instruction("je __rt_sprintf_flt_str_x64"); // yes → parse it instead of reading the pointer bits + emitter.instruction("cvtsi2sd xmm0, r10"); // widen an int/bool payload to a double + emitter.instruction("movq r10, xmm0"); // keep the double bits in the integer register + emitter.instruction("jmp __rt_sprintf_flt_bits_x64"); // the operand is a double now + emitter.label("__rt_sprintf_flt_str_x64"); + emitter.instruction("mov rax, r10"); // string pointer for the numeric parse + emitter.instruction("mov rdx, r11"); // record tag word holding the string length + emitter.instruction("shr rdx, 8"); // string byte length for the numeric parse + emitter.instruction("test rax, rax"); // is the string pointer null? + emitter.instruction("jz __rt_sprintf_flt_str_null_x64"); // yes → a null pointer parses as zero + emitter.instruction("cmp rdx, 4095"); // __rt_cstr copies into a 4096-byte scratch + emitter.instruction("jbe __rt_sprintf_flt_str_go_x64"); // the string already fits the C-string scratch + emitter.instruction("mov edx, 4095"); // clamp so the numeric prefix parse stays in bounds + emitter.label("__rt_sprintf_flt_str_go_x64"); + emitter.instruction("call __rt_str_to_number"); // PHP leading-numeric string-to-float conversion + emitter.instruction("movq r10, xmm0"); // keep the parsed double bits in the integer register + emitter.instruction("movzx r8d, BYTE PTR [rbp - 104]"); // reload the conversion character after the parse + emitter.instruction("jmp __rt_sprintf_flt_bits_x64"); // the operand is a double now + emitter.label("__rt_sprintf_flt_str_null_x64"); + emitter.instruction("xor r10d, r10d"); // a null string operand formats as zero + emitter.label("__rt_sprintf_flt_bits_x64"); + emitter.instruction("cmp r8b, 103"); // 'g' keeps PHP's negative-zero sign + emitter.instruction("je __rt_sprintf_flt_nz_x64"); // skip the negative-zero normalization + emitter.instruction("cmp r8b, 71"); // 'G' keeps PHP's negative-zero sign + emitter.instruction("je __rt_sprintf_flt_nz_x64"); // skip the negative-zero normalization + emitter.instruction("mov rax, r10"); // copy the double bits + emitter.instruction("add rax, rax"); // drop the sign bit to test for any zero + emitter.instruction("test rax, rax"); // is the value a zero of either sign? + emitter.instruction("jnz __rt_sprintf_flt_nz_x64"); // no → leave the value alone + emitter.instruction("xor r10d, r10d"); // PHP prints -0.0 as 0.000000 under %f/%e + emitter.label("__rt_sprintf_flt_nz_x64"); + emitter.instruction("lea r9, [rbp - 160]"); // mini C format cursor + emitter.instruction("mov BYTE PTR [r9], 37"); // write the '%' introducer + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("test QWORD PTR [rbp - 88], 2"); // is the force-sign flag set? + emitter.instruction("jz __rt_sprintf_flt_noplus_x64"); // no → skip the '+' flag + emitter.instruction("mov BYTE PTR [r9], 43"); // write the '+' flag + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.label("__rt_sprintf_flt_noplus_x64"); + emitter.instruction("test QWORD PTR [rbp - 88], 4"); // is the alternate-form flag set? + emitter.instruction("jz __rt_sprintf_flt_noalt_x64"); // no → skip the '#' flag + emitter.instruction("mov BYTE PTR [r9], 35"); // write the '#' flag + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.label("__rt_sprintf_flt_noalt_x64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // parsed precision + emitter.instruction("test rax, rax"); // was a precision given? + emitter.instruction("js __rt_sprintf_flt_noprec_x64"); // no → libc's default of six digits + emitter.instruction("cmp rax, 53"); // PHP caps float precision at 53 digits + emitter.instruction("jbe __rt_sprintf_flt_precok_x64"); // within the cap + emitter.instruction("mov rax, 53"); // clamp to PHP's maximum precision + emitter.label("__rt_sprintf_flt_precok_x64"); + emitter.instruction("mov BYTE PTR [r9], 46"); // write the '.' precision introducer + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("cmp rax, 10"); // does the precision need two digits? + emitter.instruction("jb __rt_sprintf_flt_prec1_x64"); // no → a single digit is enough + emitter.instruction("xor rdx, rdx"); // clear the high half of the dividend + emitter.instruction("mov rcx, 10"); // decimal radix for the split + emitter.instruction("div rcx"); // rax = tens digit, rdx = units digit + emitter.instruction("add al, 48"); // turn the tens digit into ASCII + emitter.instruction("mov BYTE PTR [r9], al"); // write the tens digit + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("add dl, 48"); // turn the units digit into ASCII + emitter.instruction("mov BYTE PTR [r9], dl"); // write the units digit + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("jmp __rt_sprintf_flt_noprec_x64"); // precision written + emitter.label("__rt_sprintf_flt_prec1_x64"); + emitter.instruction("add al, 48"); // turn the single digit into ASCII + emitter.instruction("mov BYTE PTR [r9], al"); // write the single precision digit + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.label("__rt_sprintf_flt_noprec_x64"); + emitter.instruction("mov BYTE PTR [r9], r8b"); // write the conversion character + emitter.instruction("add r9, 1"); // advance the mini format cursor + emitter.instruction("mov BYTE PTR [r9], 0"); // NUL-terminate the mini C format string + emitter.instruction("lea rdi, [rbp - 672]"); // conversion scratch destination + emitter.instruction(&format!("mov esi, {}", CONV_SCRATCH_CAP)); // conversion scratch capacity + emitter.instruction("lea rdx, [rbp - 160]"); // the mini C format string + emitter.instruction("movq xmm0, r10"); // the double operand as the first SSE variadic + emitter.instruction("mov eax, 1"); // one SSE variadic register is live + emitter.bl_c("snprintf"); // render the float body through libc + emitter.instruction("jmp __rt_sprintf_snret_x64"); // clamp and take the result +} + +/// Emits the x86_64 post-`snprintf` clamp. +/// +/// libc returns the number of bytes it *would* have written, so the value is clamped to the +/// bytes actually present in the scratch buffer before it is ever used as a length. That +/// clamp is the direct fix for the out-of-bounds stack read this helper used to have. +fn emit_snprintf_result(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_snret_x64"); + emitter.instruction("movsxd r11, eax"); // snprintf returns a signed 32-bit count + emitter.instruction("test r11, r11"); // is the count negative? + emitter.instruction("jns __rt_sprintf_snret_nn_x64"); // no → usable as a length + emitter.instruction("xor r11d, r11d"); // an encoding error produced no bytes + emitter.label("__rt_sprintf_snret_nn_x64"); + emitter.instruction(&format!("cmp r11, {}", CONV_SCRATCH_CAP - 1)); // did libc want more than the scratch holds? + emitter.instruction("jbe __rt_sprintf_snret_ok_x64"); // no → every counted byte is really there + emitter.instruction(&format!("mov r11d, {}", CONV_SCRATCH_CAP - 1)); // clamp to the bytes actually written + emitter.label("__rt_sprintf_snret_ok_x64"); + emitter.instruction("lea r10, [rbp - 672]"); // body pointer = conversion scratch + emitter.instruction("movzx r9d, BYTE PTR [rbp - 104]"); // reload the conversion character + emitter.instruction("cmp r9b, 101"); // 'e' needs PHP's exponent form + emitter.instruction("je __rt_sprintf_expfix_x64"); // compact the exponent + emitter.instruction("cmp r9b, 69"); // 'E' needs PHP's exponent form + emitter.instruction("je __rt_sprintf_expfix_x64"); // compact the exponent + emitter.instruction("jmp __rt_sprintf_emit_x64"); // pad and copy the rendered body +} + +/// Emits the x86_64 exponent compaction for `%e`/`%E`. +/// +/// C always pads the exponent to at least two digits (`1.234568e+04`) while PHP does not +/// (`1.234568e+4`), so the leading zeros of the exponent field are removed in place, always +/// leaving at least one digit. +fn emit_exponent_compaction(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_expfix_x64"); + emitter.instruction("mov rax, r10"); // read cursor over the rendered body + emitter.instruction("mov rcx, r10"); // write cursor for the compacted body + emitter.instruction("lea rdx, [r10 + r11]"); // one past the last rendered byte + emitter.label("__rt_sprintf_expfix_scan_x64"); + emitter.instruction("cmp rax, rdx"); // reached the end without an exponent? + emitter.instruction("jae __rt_sprintf_expfix_done_x64"); // yes → nothing to compact + emitter.instruction("movzx r8d, BYTE PTR [rax]"); // load the current body byte + emitter.instruction("cmp r8b, 101"); // lowercase exponent marker? + emitter.instruction("je __rt_sprintf_expfix_hit_x64"); // yes → compact from here + emitter.instruction("cmp r8b, 69"); // uppercase exponent marker? + emitter.instruction("je __rt_sprintf_expfix_hit_x64"); // yes → compact from here + emitter.instruction("mov BYTE PTR [rcx], r8b"); // keep the mantissa byte + emitter.instruction("add rax, 1"); // advance the read cursor + emitter.instruction("add rcx, 1"); // advance the write cursor + emitter.instruction("jmp __rt_sprintf_expfix_scan_x64"); // keep scanning for the exponent + emitter.label("__rt_sprintf_expfix_hit_x64"); + emitter.instruction("mov BYTE PTR [rcx], r8b"); // keep the exponent marker + emitter.instruction("add rax, 1"); // advance the read cursor + emitter.instruction("add rcx, 1"); // advance the write cursor + emitter.instruction("cmp rax, rdx"); // is there anything after the marker? + emitter.instruction("jae __rt_sprintf_expfix_done_x64"); // no → the body ends here + emitter.instruction("movzx r8d, BYTE PTR [rax]"); // load the exponent sign byte + emitter.instruction("cmp r8b, 43"); // '+' exponent sign? + emitter.instruction("je __rt_sprintf_expfix_sign_x64"); // yes → keep it + emitter.instruction("cmp r8b, 45"); // '-' exponent sign? + emitter.instruction("jne __rt_sprintf_expfix_zeros_x64"); // no sign at all → go straight to the digits + emitter.label("__rt_sprintf_expfix_sign_x64"); + emitter.instruction("mov BYTE PTR [rcx], r8b"); // keep the exponent sign + emitter.instruction("add rax, 1"); // advance the read cursor + emitter.instruction("add rcx, 1"); // advance the write cursor + emitter.label("__rt_sprintf_expfix_zeros_x64"); + emitter.instruction("lea rsi, [rdx - 1]"); // address of the final exponent digit + emitter.label("__rt_sprintf_expfix_zloop_x64"); + emitter.instruction("cmp rax, rsi"); // never drop the last exponent digit + emitter.instruction("jae __rt_sprintf_expfix_tail_x64"); // one digit left → stop stripping + emitter.instruction("movzx r8d, BYTE PTR [rax]"); // load the current exponent digit + emitter.instruction("cmp r8b, 48"); // is it a padding zero? + emitter.instruction("jne __rt_sprintf_expfix_tail_x64"); // no → the exponent starts here + emitter.instruction("add rax, 1"); // skip the padding zero + emitter.instruction("jmp __rt_sprintf_expfix_zloop_x64"); // check the next exponent digit + emitter.label("__rt_sprintf_expfix_tail_x64"); + emitter.instruction("cmp rax, rdx"); // copied every remaining byte? + emitter.instruction("jae __rt_sprintf_expfix_done_x64"); // yes → compaction finished + emitter.instruction("movzx r8d, BYTE PTR [rax]"); // load the next exponent byte + emitter.instruction("mov BYTE PTR [rcx], r8b"); // keep the exponent byte + emitter.instruction("add rax, 1"); // advance the read cursor + emitter.instruction("add rcx, 1"); // advance the write cursor + emitter.instruction("jmp __rt_sprintf_expfix_tail_x64"); // copy the rest of the exponent + emitter.label("__rt_sprintf_expfix_done_x64"); + emitter.instruction("mov r11, rcx"); // compacted end address + emitter.instruction("sub r11, r10"); // compacted body length +} + +/// Emits the x86_64 pad-and-copy stage shared by every conversion. +/// +/// `r10`/`r11` carry the conversion body. The field width is validated against PHP's +/// `0..INT_MAX` range and the whole padded result is bounds-checked against the end of +/// `_concat_buf` *before* a single byte is written, so neither an absurd width nor a long +/// body can walk off the arena. Zero padding is inserted after a leading sign, matching +/// PHP's `sprintf("%05d", -42)` → `-0042`. +fn emit_pad_and_copy(emitter: &mut Emitter) { + emitter.label("__rt_sprintf_emit_x64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // parsed field width + emitter.instruction("mov rcx, rax"); // copy it for the range test + emitter.instruction("shr rcx, 31"); // any bit above INT_MAX set? + emitter.instruction("test rcx, rcx"); // is the width out of PHP's range? + emitter.instruction("jnz __rt_sprintf_wfatal_x64"); // yes → PHP rejects the width + emitter.instruction("xor rcx, rcx"); // padding byte count + emitter.instruction("cmp rax, r11"); // is the body already at least as wide? + emitter.instruction("jbe __rt_sprintf_emit_nopad_x64"); // yes → no padding needed + emitter.instruction("mov rcx, rax"); // padding = width ... + emitter.instruction("sub rcx, r11"); // ... minus the body length + emitter.label("__rt_sprintf_emit_nopad_x64"); + emitter.instruction("mov rdx, r11"); // body length + emitter.instruction("add rdx, rcx"); // total bytes this conversion emits + emitter.instruction("add rdx, rbx"); // address just past the emitted bytes + emitter.instruction("cmp rdx, QWORD PTR [rbp - 120]"); // would the conversion leave the arena? + emitter.instruction("ja __rt_sprintf_ofatal_x64"); // yes → controlled fatal instead of an overrun + emitter.instruction("movzx r9d, BYTE PTR [rbp - 96]"); // pad character + emitter.instruction("test QWORD PTR [rbp - 88], 1"); // is the left-align flag set? + emitter.instruction("jnz __rt_sprintf_emit_left_x64"); // yes → body first, padding after + emitter.instruction("test rcx, rcx"); // is there any padding at all? + emitter.instruction("jz __rt_sprintf_emit_pad_x64"); // no → copy the body directly + emitter.instruction("cmp r9b, 48"); // only '0' padding moves ahead of the sign + emitter.instruction("jne __rt_sprintf_emit_pad_x64"); // other pad characters stay before the sign + emitter.instruction("test r11, r11"); // is the body empty? + emitter.instruction("jz __rt_sprintf_emit_pad_x64"); // yes → there is no sign to hoist + emitter.instruction("movzx r8d, BYTE PTR [r10]"); // first body byte + emitter.instruction("cmp r8b, 45"); // is it a minus sign? + emitter.instruction("je __rt_sprintf_emit_sign_x64"); // yes → emit it before the zeros + emitter.instruction("cmp r8b, 43"); // is it a plus sign? + emitter.instruction("jne __rt_sprintf_emit_pad_x64"); // no sign → pad normally + emitter.label("__rt_sprintf_emit_sign_x64"); + emitter.instruction("mov BYTE PTR [rbx], r8b"); // emit the sign ahead of the zero padding + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("add r10, 1"); // the sign is no longer part of the body + emitter.instruction("sub r11, 1"); // shorten the body accordingly + emitter.label("__rt_sprintf_emit_pad_x64"); + emitter.instruction("test rcx, rcx"); // any padding bytes left? + emitter.instruction("jz __rt_sprintf_emit_copy_x64"); // no → copy the body + emitter.instruction("mov BYTE PTR [rbx], r9b"); // emit one padding byte + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("sub rcx, 1"); // one padding byte fewer to write + emitter.instruction("jmp __rt_sprintf_emit_pad_x64"); // keep padding + emitter.label("__rt_sprintf_emit_copy_x64"); + emitter.instruction("test r11, r11"); // any body bytes left? + emitter.instruction("jz __rt_sprintf_loop_x64"); // no → scan the next format byte + emitter.instruction("movzx r8d, BYTE PTR [r10]"); // load the next body byte + emitter.instruction("mov BYTE PTR [rbx], r8b"); // emit the body byte + emitter.instruction("add r10, 1"); // advance the body cursor + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("sub r11, 1"); // one body byte fewer to copy + emitter.instruction("jmp __rt_sprintf_emit_copy_x64"); // keep copying + emitter.label("__rt_sprintf_emit_left_x64"); + emitter.instruction("test r11, r11"); // any body bytes left? + emitter.instruction("jz __rt_sprintf_emit_lpad_x64"); // no → append the padding + emitter.instruction("movzx r8d, BYTE PTR [r10]"); // load the next body byte + emitter.instruction("mov BYTE PTR [rbx], r8b"); // emit the body byte + emitter.instruction("add r10, 1"); // advance the body cursor + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("sub r11, 1"); // one body byte fewer to copy + emitter.instruction("jmp __rt_sprintf_emit_left_x64"); // keep copying + emitter.label("__rt_sprintf_emit_lpad_x64"); + emitter.instruction("test rcx, rcx"); // any trailing padding left? + emitter.instruction("jz __rt_sprintf_loop_x64"); // no → scan the next format byte + emitter.instruction("mov BYTE PTR [rbx], r9b"); // emit one trailing padding byte + emitter.instruction("add rbx, 1"); // advance the write cursor + emitter.instruction("sub rcx, 1"); // one padding byte fewer to write + emitter.instruction("jmp __rt_sprintf_emit_lpad_x64"); // keep padding +} + +/// Emits the four x86_64 controlled-fatal exits: out-of-range width, result larger than the +/// concat arena, too few arguments, and an unknown conversion character. Each writes a +/// PHP-shaped diagnostic to stderr and exits with PHP's fatal-error status (255). +fn emit_fatal_paths(emitter: &mut Emitter) { + emit_fatal(emitter, "__rt_sprintf_wfatal_x64", "_sprintf_width_msg", SPRINTF_WIDTH_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_ofatal_x64", "_sprintf_overflow_msg", SPRINTF_OVERFLOW_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_afatal_x64", "_sprintf_argcount_msg", SPRINTF_ARGCOUNT_MSG.len()); + emit_fatal(emitter, "__rt_sprintf_sfatal_x64", "_sprintf_unknown_spec_msg", SPRINTF_UNKNOWN_SPEC_MSG.len()); +} + +/// Emits one x86_64 fatal exit block: write `len` bytes of `symbol` to stderr with the +/// Linux `write` syscall, then exit with status 255 (PHP's fatal-error status). +fn emit_fatal(emitter: &mut Emitter, label: &str, symbol: &str, len: usize) { + emitter.label(label); + emitter.instruction("mov edi, 2"); // write the diagnostic to stderr + abi::emit_symbol_address(emitter, "rsi", symbol); + emitter.instruction(&format!("mov edx, {}", len)); // exact diagnostic byte length + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // emit the diagnostic before terminating + emitter.instruction("mov edi, 255"); // PHP exits with 255 on a fatal error + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall 60 = exit + emitter.instruction("syscall"); // terminate the process } diff --git a/src/codegen_support/runtime/strings/str_inc_dec.rs b/src/codegen_support/runtime/strings/str_inc_dec.rs new file mode 100644 index 0000000000..14352ba69f --- /dev/null +++ b/src/codegen_support/runtime/strings/str_inc_dec.rs @@ -0,0 +1,609 @@ +//! Purpose: +//! Emits `__rt_str_inc_dec` and `__rt_mixed_inc_dec`, the runtime implementation of PHP's +//! `++` / `--` on a string value. `__rt_str_inc_dec` takes a raw PHP byte string, applies +//! PHP's numeric-string / perl-style-alphanumeric rules, and returns the new value already +//! boxed into a Mixed cell (the operator can change the value's type, so the result is +//! always dynamically tagged). `__rt_mixed_inc_dec` is the boxed entry point: it routes a +//! string payload here and everything else to the existing numeric helper. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via +//! `crate::codegen_support::runtime::strings`. +//! - `crate::codegen::lower_inst::strings::lower_str_inc_dec()` for `Op::StrIncDec`. +//! +//! Key details: +//! - PHP's rules, verified against PHP 8.4.20: a numeric string increments NUMERICALLY and +//! changes type (`"9"++` is `int(10)`, `"1.5"++` is `float(2.5)`, and an int result that +//! overflows promotes to float); the empty string increments to `string "1"` and +//! decrements to `int(-1)`; any other string increments with perl-style alphanumeric +//! carry (`"az"++` is `"ba"`, `"Zz"++` is `"AAa"`, `"a9"++` is `"b0"`, `"zz"++` is +//! `"aaa"`) and DECREMENTS TO ITSELF (PHP leaves a non-numeric string unchanged). +//! - The carry stops at the first non-alphanumeric byte, so `"a-"++` is `"a-"` while +//! `"-a"++` is `"-b"`. Bytes are compared as raw ASCII: this is byte-oriented exactly +//! like php-src, so multi-byte characters are never carried into. +//! - The carried result is built in the shared `_concat_buf` scratch (one spare byte in +//! front for the `z`→`aa` growth) and immediately handed to `__rt_mixed_from_value`, +//! which persists it into owned heap storage; the scratch offset is deliberately NOT +//! advanced because the bytes do not outlive that call. +//! - The numeric classification is `__rt_php_num_scan`'s fully-numeric flag, so it matches +//! `is_numeric()` byte for byte (`" 5"` and `"5 "` are numeric, `"0x1A"` and `"1_0"` are +//! not, and therefore carry alphanumerically like PHP). +//! - PHP additionally raises `E_DEPRECATED` for `++` on a non-alphanumeric string and for +//! `--` on a non-numeric string. elephc has no runtime deprecation channel, so only the +//! resulting VALUE is reproduced here. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::{abi, platform::Arch}; + +/// Emits `__rt_str_inc_dec` for the active target. +/// +/// # ABI +/// - **AArch64**: `x1` = string pointer, `x2` = string length, `x3` = delta (`+1` or `-1`) +/// → `x0` = owned boxed Mixed cell holding the new PHP value. +/// - **x86_64**: `rax` = string pointer, `rdx` = string length, `rcx` = delta +/// → `rax` = owned boxed Mixed cell. +/// +/// The operand string is only read; the caller keeps its ownership. +pub fn emit_str_inc_dec(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_str_inc_dec_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: str_inc_dec (PHP ++/-- on a string) ---"); + emitter.label_global("__rt_str_inc_dec"); + + // -- frame: [sp+0] ptr, [sp+8] len, [sp+16] delta, [sp+24] numeric run, [sp+32] carry prefix -- + emitter.instruction("sub sp, sp, #64"); // reserve the helper frame for the operand, the clipped numeric run, and saved linkage + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the caller frame pointer and return address across nested runtime calls + emitter.instruction("add x29, sp, #48"); // establish the helper frame pointer above the saved loop state + emitter.instruction("str x1, [sp, #0]"); // save the operand string pointer for the carry and unchanged paths + emitter.instruction("str x2, [sp, #8]"); // save the operand string length for the carry and unchanged paths + emitter.instruction("str x3, [sp, #16]"); // save the +1/-1 delta for every result path + emitter.instruction("cbz x2, __rt_sid_empty"); // the empty string has its own PHP rules and never reaches the scanner + + // -- classify the operand under PHP's numeric-string grammar -- + emitter.instruction("bl __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer + emitter.instruction("bl __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("str x0, [sp, #24]"); // save the clipped numeric run for the integer parser and strtod + emitter.instruction("cbz x1, __rt_sid_alpha"); // a string PHP does not consider numeric carries alphanumerically + + // -- a '.' or an exponent marker in the run forces PHP's float result -- + emitter.instruction("mov x9, x0"); // x9 = cursor over the clipped numeric run + emitter.label("__rt_sid_scan"); + emitter.instruction("ldrb w10, [x9]"); // load the next byte of the clipped numeric run + emitter.instruction("cbz w10, __rt_sid_int"); // an integer-shaped run ends without any float marker + emitter.instruction("cmp w10, #46"); // ASCII '.' makes the numeric string a float in PHP + emitter.instruction("b.eq __rt_sid_float"); // a decimal point selects the float result path + emitter.instruction("orr w11, w10, #32"); // fold the byte to lowercase so 'E' and 'e' compare alike + emitter.instruction("cmp w11, #101"); // ASCII 'e' introduces PHP's exponent form, which is always a float + emitter.instruction("b.eq __rt_sid_float"); // an exponent marker selects the float result path + emitter.instruction("add x9, x9, #1"); // advance to the next byte of the numeric run + emitter.instruction("b __rt_sid_scan"); // keep scanning until the run ends or a float marker appears + + // -- integer-shaped numeric string: parse the magnitude exactly, detecting overflow -- + emitter.label("__rt_sid_int"); + emitter.instruction("ldr x9, [sp, #24]"); // x9 = cursor back at the start of the clipped numeric run + emitter.instruction("mov x12, #0"); // x12 = 1 once a leading '-' marks the run negative + emitter.instruction("ldrb w10, [x9]"); // load the optional sign byte of the numeric run + emitter.instruction("cmp w10, #45"); // ASCII '-' introduces a negative numeric string + emitter.instruction("b.ne __rt_sid_int_plus"); // no minus sign: check for an explicit plus instead + emitter.instruction("mov x12, #1"); // remember that the parsed magnitude must be negated + emitter.instruction("add x9, x9, #1"); // consume the minus sign before the digits + emitter.instruction("b __rt_sid_int_digits"); // continue with the digit run + emitter.label("__rt_sid_int_plus"); + emitter.instruction("cmp w10, #43"); // ASCII '+' is also allowed in front of a PHP numeric string + emitter.instruction("b.ne __rt_sid_int_digits"); // no sign at all: the digits start here + emitter.instruction("add x9, x9, #1"); // consume the plus sign before the digits + + emitter.label("__rt_sid_int_digits"); + emitter.instruction("mov x14, #0"); // x14 = the accumulated unsigned magnitude + emitter.instruction("mov x16, #10"); // x16 = the decimal radix used by the accumulation + emitter.label("__rt_sid_int_loop"); + emitter.instruction("ldrb w10, [x9]"); // load the next candidate digit of the numeric run + emitter.instruction("sub w15, w10, #48"); // normalize the byte into a 0..9 digit value + emitter.instruction("cmp w15, #9"); // is this byte still a decimal digit? + emitter.instruction("b.hi __rt_sid_int_parsed"); // the digit run has ended, so the magnitude is complete + emitter.instruction("umulh x17, x14, x16"); // compute the high half of magnitude * 10 to detect 64-bit overflow + emitter.instruction("cbnz x17, __rt_sid_float"); // a magnitude past 64 bits is PHP's float result + emitter.instruction("mul x14, x14, x16"); // shift the accumulated magnitude one decimal place + emitter.instruction("adds x14, x14, x15"); // add the new digit and record any carry out of 64 bits + emitter.instruction("b.cs __rt_sid_float"); // an unsigned carry means the magnitude no longer fits, so PHP uses a float + emitter.instruction("add x9, x9, #1"); // advance past the consumed digit + emitter.instruction("b __rt_sid_int_loop"); // keep accumulating until the digit run ends + + emitter.label("__rt_sid_int_parsed"); + emitter.instruction("cbnz x12, __rt_sid_int_negative"); // a negative run has a different magnitude bound than a positive one + emitter.instruction("mov x16, #-1"); // start from an all-ones word to materialize the signed maximum + emitter.instruction("lsr x16, x16, #1"); // x16 = 0x7fffffffffffffff, the largest PHP integer + emitter.instruction("cmp x14, x16"); // does the parsed magnitude still fit in a PHP integer? + emitter.instruction("b.hi __rt_sid_float"); // a magnitude above PHP_INT_MAX is a float numeric string + emitter.instruction("mov x15, x14"); // x15 = the signed value of a positive numeric string + emitter.instruction("b __rt_sid_int_value"); // continue with the shared integer increment + emitter.label("__rt_sid_int_negative"); + emitter.instruction("mov x16, #1"); // start from one to materialize the magnitude of PHP_INT_MIN + emitter.instruction("lsl x16, x16, #63"); // x16 = 0x8000000000000000, the magnitude of the smallest PHP integer + emitter.instruction("cmp x14, x16"); // does the negative magnitude still fit in a PHP integer? + emitter.instruction("b.hi __rt_sid_float"); // a magnitude below PHP_INT_MIN is a float numeric string + emitter.instruction("neg x15, x14"); // x15 = the signed value of a negative numeric string + + emitter.label("__rt_sid_int_value"); + emitter.instruction("ldr x3, [sp, #16]"); // reload the +1/-1 delta for the integer increment + emitter.instruction("adds x15, x15, x3"); // apply the increment and record signed overflow + emitter.instruction("b.vs __rt_sid_float"); // PHP promotes an overflowing integer increment to a float + emitter.instruction("mov x1, x15"); // pass the new integer as the boxing helper's low payload word + emitter.instruction("mov x2, xzr"); // integer payloads do not use a second word + emitter.instruction("mov x0, #0"); // runtime tag 0 = int + emitter.instruction("bl __rt_mixed_from_value"); // box the incremented integer for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + + // -- float-shaped (or out-of-range) numeric string: reparse and add the delta as a double -- + emitter.label("__rt_sid_float"); + emitter.instruction("ldr x0, [sp, #24]"); // reload the clipped numeric run for the libc parser + emitter.instruction("mov x1, #0"); // strtod endptr = NULL: the run is already clipped + emitter.bl_c("strtod"); // parse the clipped numeric run into d0 + emitter.instruction("ldr x3, [sp, #16]"); // reload the +1/-1 delta for the float increment + emitter.instruction("scvtf d1, x3"); // convert the delta into a double so the addition is exact + emitter.instruction("fadd d0, d0, d1"); // apply PHP's float increment to the parsed value + emitter.instruction("fmov x1, d0"); // move the resulting double bits into the boxing helper payload register + emitter.instruction("mov x2, xzr"); // float payloads only use the low word + emitter.instruction("mov x0, #2"); // runtime tag 2 = float + emitter.instruction("bl __rt_mixed_from_value"); // box the incremented double for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + + // -- the empty string: PHP yields string "1" for ++ and int(-1) for -- -- + emitter.label("__rt_sid_empty"); + emitter.instruction("ldr x3, [sp, #16]"); // reload the delta to tell the two empty-string rules apart + emitter.instruction("cmp x3, #0"); // is this a decrement of the empty string? + emitter.instruction("b.lt __rt_sid_empty_dec"); // decrementing the empty string yields PHP's int(-1) + abi::emit_symbol_address(emitter, "x9", "_concat_buf"); + emitter.instruction("mov w10, #49"); // ASCII '1' is the whole result of incrementing the empty string + emitter.instruction("strb w10, [x9]"); // materialize the one-byte result in the shared scratch buffer + emitter.instruction("mov x1, x9"); // pass the scratch pointer as the boxing helper's string payload + emitter.instruction("mov x2, #1"); // the incremented empty string is exactly one byte long + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist the one-byte result and box it for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + emitter.label("__rt_sid_empty_dec"); + emitter.instruction("mov x1, #-1"); // decrementing the empty string yields PHP's int(-1) + emitter.instruction("mov x2, xzr"); // integer payloads do not use a second word + emitter.instruction("mov x0, #0"); // runtime tag 0 = int + emitter.instruction("bl __rt_mixed_from_value"); // box the int(-1) result for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + + // -- non-numeric string: '--' is a no-op, '++' carries alphanumerically -- + emitter.label("__rt_sid_alpha"); + emitter.instruction("ldr x3, [sp, #16]"); // reload the delta to separate the increment from the decrement rule + emitter.instruction("cmp x3, #0"); // is this a decrement of a non-numeric string? + emitter.instruction("b.gt __rt_sid_carry"); // only the increment applies PHP's perl-style carry + emitter.instruction("ldr x1, [sp, #0]"); // PHP leaves a decremented non-numeric string unchanged + emitter.instruction("ldr x2, [sp, #8]"); // reload the unchanged string length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist the unchanged string and box it for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + + // -- copy the operand into scratch, leaving one spare byte for a 'z' -> 'aa' growth -- + emitter.label("__rt_sid_carry"); + abi::emit_symbol_address(emitter, "x6", "_concat_off"); + emitter.instruction("ldr x8, [x6]"); // load the current shared scratch write offset + abi::emit_symbol_address(emitter, "x7", "_concat_buf"); + emitter.instruction("add x9, x7, x8"); // compute the scratch cursor for this result + emitter.instruction("add x9, x9, #16"); // keep a header-sized gap so the heap-kind probe never reads before the buffer + emitter.instruction("ldr x10, [sp, #0]"); // x10 = source cursor over the operand string + emitter.instruction("ldr x11, [sp, #8]"); // x11 = the operand string length + emitter.instruction("add x12, x9, #1"); // x12 = destination cursor, one byte past the growth slot + emitter.instruction("mov x13, x11"); // x13 = remaining bytes to copy + emitter.label("__rt_sid_copy"); + emitter.instruction("cbz x13, __rt_sid_copied"); // stop once the whole operand has been copied into scratch + emitter.instruction("ldrb w14, [x10], #1"); // load one operand byte and advance the source cursor + emitter.instruction("strb w14, [x12], #1"); // store the byte into scratch and advance the destination cursor + emitter.instruction("sub x13, x13, #1"); // account for the copied byte + emitter.instruction("b __rt_sid_copy"); // continue until the operand is fully copied + + // -- carry from the last byte towards the front, exactly like php-src -- + emitter.label("__rt_sid_copied"); + emitter.instruction("add x12, x9, #1"); // x12 = base of the mutable copy inside scratch + emitter.instruction("mov x13, x11"); // x13 = one past the byte position still to be processed + emitter.label("__rt_sid_carry_loop"); + emitter.instruction("cbz x13, __rt_sid_carry_escaped"); // a carry out of position zero grows the string by one byte + emitter.instruction("sub x14, x13, #1"); // x14 = the byte position currently being incremented + emitter.instruction("ldrb w15, [x12, x14]"); // load the byte the carry has reached + emitter.instruction("cmp w15, #97"); // is the byte below lowercase 'a'? + emitter.instruction("b.lo __rt_sid_not_lower"); // check the uppercase and digit ranges instead + emitter.instruction("cmp w15, #122"); // is the byte above lowercase 'z'? + emitter.instruction("b.hi __rt_sid_stop"); // a byte past 'z' is not alphanumeric and stops the carry + emitter.instruction("cmp w15, #122"); // is the byte exactly lowercase 'z'? + emitter.instruction("b.eq __rt_sid_wrap_lower"); // 'z' wraps to 'a' and carries into the previous byte + emitter.instruction("add w15, w15, #1"); // any other lowercase letter simply advances by one + emitter.instruction("strb w15, [x12, x14]"); // store the advanced letter back into the scratch copy + emitter.instruction("b __rt_sid_stop"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_lower"); + emitter.instruction("mov w15, #97"); // 'z' wraps around to lowercase 'a' + emitter.instruction("strb w15, [x12, x14]"); // store the wrapped letter back into the scratch copy + emitter.instruction("mov w15, #97"); // a lowercase carry out of the string prepends another 'a' + emitter.instruction("str x15, [sp, #32]"); // remember the prefix byte in case the carry escapes the string + emitter.instruction("b __rt_sid_carry_next"); // continue the carry into the previous byte + emitter.label("__rt_sid_not_lower"); + emitter.instruction("cmp w15, #65"); // is the byte below uppercase 'A'? + emitter.instruction("b.lo __rt_sid_not_upper"); // check the digit range instead + emitter.instruction("cmp w15, #90"); // is the byte above uppercase 'Z'? + emitter.instruction("b.hi __rt_sid_stop"); // a byte between 'Z' and 'a' is not alphanumeric and stops the carry + emitter.instruction("cmp w15, #90"); // is the byte exactly uppercase 'Z'? + emitter.instruction("b.eq __rt_sid_wrap_upper"); // 'Z' wraps to 'A' and carries into the previous byte + emitter.instruction("add w15, w15, #1"); // any other uppercase letter simply advances by one + emitter.instruction("strb w15, [x12, x14]"); // store the advanced letter back into the scratch copy + emitter.instruction("b __rt_sid_stop"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_upper"); + emitter.instruction("mov w15, #65"); // 'Z' wraps around to uppercase 'A' + emitter.instruction("strb w15, [x12, x14]"); // store the wrapped letter back into the scratch copy + emitter.instruction("mov w15, #65"); // an uppercase carry out of the string prepends another 'A' + emitter.instruction("str x15, [sp, #32]"); // remember the prefix byte in case the carry escapes the string + emitter.instruction("b __rt_sid_carry_next"); // continue the carry into the previous byte + emitter.label("__rt_sid_not_upper"); + emitter.instruction("cmp w15, #48"); // is the byte below digit '0'? + emitter.instruction("b.lo __rt_sid_stop"); // a byte below '0' is not alphanumeric and stops the carry + emitter.instruction("cmp w15, #57"); // is the byte above digit '9'? + emitter.instruction("b.hi __rt_sid_stop"); // a byte between '9' and 'A' is not alphanumeric and stops the carry + emitter.instruction("cmp w15, #57"); // is the byte exactly digit '9'? + emitter.instruction("b.eq __rt_sid_wrap_digit"); // '9' wraps to '0' and carries into the previous byte + emitter.instruction("add w15, w15, #1"); // any other digit simply advances by one + emitter.instruction("strb w15, [x12, x14]"); // store the advanced digit back into the scratch copy + emitter.instruction("b __rt_sid_stop"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_digit"); + emitter.instruction("mov w15, #48"); // '9' wraps around to digit '0' + emitter.instruction("strb w15, [x12, x14]"); // store the wrapped digit back into the scratch copy + emitter.instruction("mov w15, #49"); // a digit carry out of the string prepends a '1' + emitter.instruction("str x15, [sp, #32]"); // remember the prefix byte in case the carry escapes the string + emitter.label("__rt_sid_carry_next"); + emitter.instruction("sub x13, x13, #1"); // move the carry one byte towards the front of the string + emitter.instruction("b __rt_sid_carry_loop"); // keep carrying until it is absorbed or escapes + + emitter.label("__rt_sid_stop"); + emitter.instruction("add x1, x9, #1"); // the result starts at the copy, leaving the growth slot unused + emitter.instruction("mov x2, x11"); // an absorbed carry keeps the original length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist the carried result and box it for the caller + emitter.instruction("b __rt_sid_return"); // share the epilogue with every other result path + + emitter.label("__rt_sid_carry_escaped"); + emitter.instruction("ldr x15, [sp, #32]"); // reload the prefix byte chosen by the last wrap + emitter.instruction("strb w15, [x9]"); // write the prefix into the reserved growth slot + emitter.instruction("mov x1, x9"); // the grown result starts one byte earlier + emitter.instruction("add x2, x11, #1"); // an escaped carry makes the result one byte longer + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist the grown result and box it for the caller + + emitter.label("__rt_sid_return"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the caller frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed Mixed result in x0 +} + +/// Emits the Linux x86_64 implementation of `__rt_str_inc_dec`. +/// +/// Same contract and same PHP rules as the AArch64 helper: `rax` = string pointer, +/// `rdx` = string length, `rcx` = delta → `rax` = owned boxed Mixed cell. +fn emit_str_inc_dec_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: str_inc_dec (PHP ++/-- on a string) ---"); + emitter.label_global("__rt_str_inc_dec"); + + // -- frame: [rbp-8] ptr, [rbp-16] len, [rbp-24] delta, [rbp-32] numeric run, [rbp-40] carry prefix -- + emitter.instruction("push rbp"); // preserve the caller frame pointer before nested runtime and libc calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the helper locals + emitter.instruction("sub rsp, 48"); // reserve aligned slots for the operand, the clipped run, and the carry prefix + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the operand string pointer for the carry and unchanged paths + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the operand string length for the carry and unchanged paths + emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // save the +1/-1 delta for every result path + emitter.instruction("test rdx, rdx"); // does the operand hold any byte at all? + emitter.instruction("jz __rt_sid_empty_x86"); // the empty string has its own PHP rules and never reaches the scanner + + // -- classify the operand under PHP's numeric-string grammar -- + emitter.instruction("call __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer + emitter.instruction("mov rdi, rax"); // pass the C-string pointer to the numeric-grammar scanner + emitter.instruction("call __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the clipped numeric run for the integer parser and strtod + emitter.instruction("test rdx, rdx"); // did the scanner report the whole string as numeric? + emitter.instruction("jz __rt_sid_alpha_x86"); // a string PHP does not consider numeric carries alphanumerically + + // -- a '.' or an exponent marker in the run forces PHP's float result -- + emitter.instruction("mov r8, rax"); // r8 = cursor over the clipped numeric run + emitter.label("__rt_sid_scan_x86"); + emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the next byte of the clipped numeric run + emitter.instruction("test r9b, r9b"); // has the clipped run reached its terminator? + emitter.instruction("jz __rt_sid_int_x86"); // an integer-shaped run ends without any float marker + emitter.instruction("cmp r9b, 46"); // ASCII '.' makes the numeric string a float in PHP + emitter.instruction("je __rt_sid_float_x86"); // a decimal point selects the float result path + emitter.instruction("or r9b, 32"); // fold the byte to lowercase so 'E' and 'e' compare alike + emitter.instruction("cmp r9b, 101"); // ASCII 'e' introduces PHP's exponent form, which is always a float + emitter.instruction("je __rt_sid_float_x86"); // an exponent marker selects the float result path + emitter.instruction("add r8, 1"); // advance to the next byte of the numeric run + emitter.instruction("jmp __rt_sid_scan_x86"); // keep scanning until the run ends or a float marker appears + + // -- integer-shaped numeric string: parse the magnitude exactly, detecting overflow -- + emitter.label("__rt_sid_int_x86"); + emitter.instruction("mov r8, QWORD PTR [rbp - 32]"); // r8 = cursor back at the start of the clipped numeric run + emitter.instruction("xor r11d, r11d"); // r11 = 1 once a leading '-' marks the run negative + emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the optional sign byte of the numeric run + emitter.instruction("cmp r9b, 45"); // ASCII '-' introduces a negative numeric string + emitter.instruction("jne __rt_sid_int_plus_x86"); // no minus sign: check for an explicit plus instead + emitter.instruction("mov r11d, 1"); // remember that the parsed magnitude must be negated + emitter.instruction("add r8, 1"); // consume the minus sign before the digits + emitter.instruction("jmp __rt_sid_int_digits_x86"); // continue with the digit run + emitter.label("__rt_sid_int_plus_x86"); + emitter.instruction("cmp r9b, 43"); // ASCII '+' is also allowed in front of a PHP numeric string + emitter.instruction("jne __rt_sid_int_digits_x86"); // no sign at all: the digits start here + emitter.instruction("add r8, 1"); // consume the plus sign before the digits + + emitter.label("__rt_sid_int_digits_x86"); + emitter.instruction("xor r10d, r10d"); // r10 = the accumulated unsigned magnitude + emitter.label("__rt_sid_int_loop_x86"); + emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the next candidate digit of the numeric run + emitter.instruction("sub r9d, 48"); // normalize the byte into a 0..9 digit value + emitter.instruction("cmp r9d, 9"); // is this byte still a decimal digit? + emitter.instruction("ja __rt_sid_int_parsed_x86"); // the digit run has ended, so the magnitude is complete + emitter.instruction("mov rax, r10"); // stage the accumulated magnitude in the multiplier's implicit operand + emitter.instruction("mov rcx, 10"); // the decimal radix used by the accumulation + emitter.instruction("mul rcx"); // multiply the magnitude by ten, leaving any overflow in rdx + emitter.instruction("test rdx, rdx"); // did the decimal shift leave the 64-bit range? + emitter.instruction("jnz __rt_sid_float_x86"); // a magnitude past 64 bits is PHP's float result + emitter.instruction("movsxd rcx, r9d"); // widen the parsed digit before adding it to the magnitude + emitter.instruction("add rax, rcx"); // add the new digit and record any carry out of 64 bits + emitter.instruction("jc __rt_sid_float_x86"); // an unsigned carry means the magnitude no longer fits, so PHP uses a float + emitter.instruction("mov r10, rax"); // keep the updated magnitude for the next digit + emitter.instruction("add r8, 1"); // advance past the consumed digit + emitter.instruction("jmp __rt_sid_int_loop_x86"); // keep accumulating until the digit run ends + + emitter.label("__rt_sid_int_parsed_x86"); + emitter.instruction("test r11, r11"); // is the parsed numeric string negative? + emitter.instruction("jnz __rt_sid_int_negative_x86"); // a negative run has a different magnitude bound than a positive one + emitter.instruction("mov rcx, 0x7fffffffffffffff"); // the largest PHP integer bounds a positive numeric string + emitter.instruction("cmp r10, rcx"); // does the parsed magnitude still fit in a PHP integer? + emitter.instruction("ja __rt_sid_float_x86"); // a magnitude above PHP_INT_MAX is a float numeric string + emitter.instruction("mov rax, r10"); // rax = the signed value of a positive numeric string + emitter.instruction("jmp __rt_sid_int_value_x86"); // continue with the shared integer increment + emitter.label("__rt_sid_int_negative_x86"); + emitter.instruction("mov rcx, 0x8000000000000000"); // the magnitude of PHP_INT_MIN bounds a negative numeric string + emitter.instruction("cmp r10, rcx"); // does the negative magnitude still fit in a PHP integer? + emitter.instruction("ja __rt_sid_float_x86"); // a magnitude below PHP_INT_MIN is a float numeric string + emitter.instruction("mov rax, r10"); // stage the magnitude before turning it into a negative value + emitter.instruction("neg rax"); // rax = the signed value of a negative numeric string + + emitter.label("__rt_sid_int_value_x86"); + emitter.instruction("add rax, QWORD PTR [rbp - 24]"); // apply the +1/-1 delta and record signed overflow + emitter.instruction("jo __rt_sid_float_x86"); // PHP promotes an overflowing integer increment to a float + emitter.instruction("mov rdi, rax"); // pass the new integer as the boxing helper's low payload word + emitter.instruction("xor esi, esi"); // integer payloads do not use a second word + emitter.instruction("xor eax, eax"); // runtime tag 0 = int + emitter.instruction("call __rt_mixed_from_value"); // box the incremented integer for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + + // -- float-shaped (or out-of-range) numeric string: reparse and add the delta as a double -- + emitter.label("__rt_sid_float_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the clipped numeric run for the libc parser + emitter.instruction("xor esi, esi"); // strtod endptr = NULL: the run is already clipped + emitter.instruction("call strtod"); // parse the clipped numeric run into xmm0 + emitter.instruction("cvtsi2sd xmm1, QWORD PTR [rbp - 24]"); // convert the +1/-1 delta into a double so the addition is exact + emitter.instruction("addsd xmm0, xmm1"); // apply PHP's float increment to the parsed value + emitter.instruction("movq rdi, xmm0"); // move the resulting double bits into the boxing helper payload register + emitter.instruction("xor esi, esi"); // float payloads only use the low word + emitter.instruction("mov rax, 2"); // runtime tag 2 = float + emitter.instruction("call __rt_mixed_from_value"); // box the incremented double for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + + // -- the empty string: PHP yields string "1" for ++ and int(-1) for -- -- + emitter.label("__rt_sid_empty_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // is this a decrement of the empty string? + emitter.instruction("jl __rt_sid_empty_dec_x86"); // decrementing the empty string yields PHP's int(-1) + abi::emit_symbol_address(emitter, "r8", "_concat_buf"); + emitter.instruction("mov BYTE PTR [r8], 49"); // ASCII '1' is the whole result of incrementing the empty string + emitter.instruction("mov rdi, r8"); // pass the scratch pointer as the boxing helper's string payload + emitter.instruction("mov rsi, 1"); // the incremented empty string is exactly one byte long + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist the one-byte result and box it for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + emitter.label("__rt_sid_empty_dec_x86"); + emitter.instruction("mov rdi, -1"); // decrementing the empty string yields PHP's int(-1) + emitter.instruction("xor esi, esi"); // integer payloads do not use a second word + emitter.instruction("xor eax, eax"); // runtime tag 0 = int + emitter.instruction("call __rt_mixed_from_value"); // box the int(-1) result for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + + // -- non-numeric string: '--' is a no-op, '++' carries alphanumerically -- + emitter.label("__rt_sid_alpha_x86"); + emitter.instruction("cmp QWORD PTR [rbp - 24], 0"); // is this a decrement of a non-numeric string? + emitter.instruction("jg __rt_sid_carry_x86"); // only the increment applies PHP's perl-style carry + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // PHP leaves a decremented non-numeric string unchanged + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the unchanged string length + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist the unchanged string and box it for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + + // -- copy the operand into scratch, leaving one spare byte for a 'z' -> 'aa' growth -- + emitter.label("__rt_sid_carry_x86"); + abi::emit_symbol_address(emitter, "rcx", "_concat_off"); + emitter.instruction("mov rcx, QWORD PTR [rcx]"); // load the current shared scratch write offset + abi::emit_symbol_address(emitter, "r8", "_concat_buf"); + emitter.instruction("add r8, rcx"); // compute the scratch cursor for this result + emitter.instruction("add r8, 16"); // keep a header-sized gap so the heap-kind probe never reads before the buffer + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // r9 = source cursor over the operand string + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // r10 = the operand string length + emitter.instruction("lea r11, [r8 + 1]"); // r11 = destination cursor, one byte past the growth slot + emitter.instruction("mov rcx, r10"); // rcx = remaining bytes to copy + emitter.label("__rt_sid_copy_x86"); + emitter.instruction("test rcx, rcx"); // stop once the whole operand has been copied into scratch + emitter.instruction("jz __rt_sid_copied_x86"); // the scratch copy is complete + emitter.instruction("mov al, BYTE PTR [r9]"); // load one operand byte from the source cursor + emitter.instruction("mov BYTE PTR [r11], al"); // store the byte into the scratch copy + emitter.instruction("add r9, 1"); // advance the source cursor + emitter.instruction("add r11, 1"); // advance the destination cursor + emitter.instruction("sub rcx, 1"); // account for the copied byte + emitter.instruction("jmp __rt_sid_copy_x86"); // continue until the operand is fully copied + + // -- carry from the last byte towards the front, exactly like php-src -- + emitter.label("__rt_sid_copied_x86"); + emitter.instruction("lea r11, [r8 + 1]"); // r11 = base of the mutable copy inside scratch + emitter.instruction("mov rcx, r10"); // rcx = one past the byte position still to be processed + emitter.label("__rt_sid_carry_loop_x86"); + emitter.instruction("test rcx, rcx"); // has the carry moved past the front of the string? + emitter.instruction("jz __rt_sid_carry_escaped_x86"); // a carry out of position zero grows the string by one byte + emitter.instruction("movzx eax, BYTE PTR [r11 + rcx - 1]"); // load the byte the carry has reached + emitter.instruction("cmp al, 97"); // is the byte below lowercase 'a'? + emitter.instruction("jb __rt_sid_not_lower_x86"); // check the uppercase and digit ranges instead + emitter.instruction("cmp al, 122"); // is the byte above lowercase 'z'? + emitter.instruction("ja __rt_sid_stop_x86"); // a byte past 'z' is not alphanumeric and stops the carry + emitter.instruction("cmp al, 122"); // is the byte exactly lowercase 'z'? + emitter.instruction("je __rt_sid_wrap_lower_x86"); // 'z' wraps to 'a' and carries into the previous byte + emitter.instruction("add al, 1"); // any other lowercase letter simply advances by one + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], al"); // store the advanced letter back into the scratch copy + emitter.instruction("jmp __rt_sid_stop_x86"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_lower_x86"); + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], 97"); // 'z' wraps around to lowercase 'a' + emitter.instruction("mov QWORD PTR [rbp - 40], 97"); // a lowercase carry out of the string prepends another 'a' + emitter.instruction("jmp __rt_sid_carry_next_x86"); // continue the carry into the previous byte + emitter.label("__rt_sid_not_lower_x86"); + emitter.instruction("cmp al, 65"); // is the byte below uppercase 'A'? + emitter.instruction("jb __rt_sid_not_upper_x86"); // check the digit range instead + emitter.instruction("cmp al, 90"); // is the byte above uppercase 'Z'? + emitter.instruction("ja __rt_sid_stop_x86"); // a byte between 'Z' and 'a' is not alphanumeric and stops the carry + emitter.instruction("cmp al, 90"); // is the byte exactly uppercase 'Z'? + emitter.instruction("je __rt_sid_wrap_upper_x86"); // 'Z' wraps to 'A' and carries into the previous byte + emitter.instruction("add al, 1"); // any other uppercase letter simply advances by one + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], al"); // store the advanced letter back into the scratch copy + emitter.instruction("jmp __rt_sid_stop_x86"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_upper_x86"); + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], 65"); // 'Z' wraps around to uppercase 'A' + emitter.instruction("mov QWORD PTR [rbp - 40], 65"); // an uppercase carry out of the string prepends another 'A' + emitter.instruction("jmp __rt_sid_carry_next_x86"); // continue the carry into the previous byte + emitter.label("__rt_sid_not_upper_x86"); + emitter.instruction("cmp al, 48"); // is the byte below digit '0'? + emitter.instruction("jb __rt_sid_stop_x86"); // a byte below '0' is not alphanumeric and stops the carry + emitter.instruction("cmp al, 57"); // is the byte above digit '9'? + emitter.instruction("ja __rt_sid_stop_x86"); // a byte between '9' and 'A' is not alphanumeric and stops the carry + emitter.instruction("cmp al, 57"); // is the byte exactly digit '9'? + emitter.instruction("je __rt_sid_wrap_digit_x86"); // '9' wraps to '0' and carries into the previous byte + emitter.instruction("add al, 1"); // any other digit simply advances by one + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], al"); // store the advanced digit back into the scratch copy + emitter.instruction("jmp __rt_sid_stop_x86"); // the carry is absorbed, so the result is complete + emitter.label("__rt_sid_wrap_digit_x86"); + emitter.instruction("mov BYTE PTR [r11 + rcx - 1], 48"); // '9' wraps around to digit '0' + emitter.instruction("mov QWORD PTR [rbp - 40], 49"); // a digit carry out of the string prepends a '1' + emitter.label("__rt_sid_carry_next_x86"); + emitter.instruction("sub rcx, 1"); // move the carry one byte towards the front of the string + emitter.instruction("jmp __rt_sid_carry_loop_x86"); // keep carrying until it is absorbed or escapes + + emitter.label("__rt_sid_stop_x86"); + emitter.instruction("lea rdi, [r8 + 1]"); // the result starts at the copy, leaving the growth slot unused + emitter.instruction("mov rsi, r10"); // an absorbed carry keeps the original length + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist the carried result and box it for the caller + emitter.instruction("jmp __rt_sid_return_x86"); // share the epilogue with every other result path + + emitter.label("__rt_sid_carry_escaped_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the prefix byte chosen by the last wrap + emitter.instruction("mov BYTE PTR [r8], al"); // write the prefix into the reserved growth slot + emitter.instruction("mov rdi, r8"); // the grown result starts one byte earlier + emitter.instruction("lea rsi, [r10 + 1]"); // an escaped carry makes the result one byte longer + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist the grown result and box it for the caller + + emitter.label("__rt_sid_return_x86"); + emitter.instruction("mov rsp, rbp"); // release every helper-local slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the boxed Mixed result in rax +} + +/// Emits `__rt_mixed_inc_dec`, the boxed entry point for PHP's `++` / `--`. +/// +/// A string payload is routed to [`emit_str_inc_dec`]'s helper so PHP's string rules apply; +/// every other payload keeps the pre-existing numeric behavior by boxing the delta and +/// reusing `__rt_mixed_numeric_add` (adding `-1` is `- 1` for both the integer and the +/// float paths, so one helper covers `++` and `--`). +/// +/// # ABI +/// - **AArch64**: `x0` = borrowed boxed Mixed cell, `x1` = delta → `x0` = owned Mixed cell. +/// - **x86_64**: `rax` = borrowed boxed Mixed cell, `rdi` = delta → `rax` = owned Mixed cell. +pub fn emit_mixed_inc_dec(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_mixed_inc_dec_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: mixed_inc_dec (PHP ++/-- on a boxed value) ---"); + emitter.label_global("__rt_mixed_inc_dec"); + + emitter.instruction("sub sp, sp, #48"); // reserve the helper frame for the operand, the delta, and the temporaries + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the caller frame pointer and return address across nested calls + emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer above the saved state + emitter.instruction("str x0, [sp, #0]"); // save the borrowed operand cell for the numeric path + emitter.instruction("str x1, [sp, #8]"); // save the +1/-1 delta for both result paths + emitter.instruction("bl __rt_mixed_unbox"); // read the operand's runtime tag and payload words + emitter.instruction("cmp x0, #1"); // does the boxed operand hold a string payload? + emitter.instruction("b.ne __rt_mid_numeric"); // every other payload keeps the existing numeric behavior + emitter.instruction("ldr x3, [sp, #8]"); // reload the delta as the string helper's third argument + emitter.instruction("bl __rt_str_inc_dec"); // apply PHP's string increment/decrement rules + emitter.instruction("b __rt_mid_return"); // share the epilogue with the numeric path + + emitter.label("__rt_mid_numeric"); + emitter.instruction("ldr x1, [sp, #8]"); // the delta becomes the right-hand operand of the numeric addition + emitter.instruction("mov x2, xzr"); // integer payloads do not use a second word + emitter.instruction("mov x0, #0"); // runtime tag 0 = int + emitter.instruction("bl __rt_mixed_from_value"); // box the delta so the shared numeric helper can consume it + emitter.instruction("str x0, [sp, #16]"); // keep the boxed delta so it can be released afterwards + emitter.instruction("mov x1, x0"); // pass the boxed delta as the right-hand operand + emitter.instruction("ldr x0, [sp, #0]"); // reload the borrowed operand cell as the left-hand operand + emitter.instruction("bl __rt_mixed_numeric_add"); // reuse PHP's boxed numeric addition, including overflow promotion + emitter.instruction("str x0, [sp, #24]"); // preserve the boxed result while the delta temporary is released + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed delta temporary + emitter.instruction("bl __rt_decref_mixed"); // release the temporary so the increment does not leak one cell per use + emitter.instruction("ldr x0, [sp, #24]"); // restore the boxed result for the caller + + emitter.label("__rt_mid_return"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the caller frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the boxed Mixed result in x0 +} + +/// Emits the Linux x86_64 implementation of `__rt_mixed_inc_dec`. +/// +/// Same contract as the AArch64 helper: `rax` = borrowed boxed Mixed cell, `rdi` = delta +/// → `rax` = owned boxed Mixed cell. +fn emit_mixed_inc_dec_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: mixed_inc_dec (PHP ++/-- on a boxed value) ---"); + emitter.label_global("__rt_mixed_inc_dec"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer before nested runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the helper locals + emitter.instruction("sub rsp, 48"); // reserve aligned slots for the operand, the delta, and the temporaries + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed operand cell for the numeric path + emitter.instruction("mov QWORD PTR [rbp - 16], rdi"); // save the +1/-1 delta for both result paths + emitter.instruction("call __rt_mixed_unbox"); // read the operand's runtime tag and payload words + emitter.instruction("cmp rax, 1"); // does the boxed operand hold a string payload? + emitter.instruction("jne __rt_mid_numeric_x86"); // every other payload keeps the existing numeric behavior + emitter.instruction("mov rax, rdi"); // the unboxed payload low word is the string pointer the helper expects in rax + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // reload the delta as the string helper's third argument (the length is already in rdx) + emitter.instruction("call __rt_str_inc_dec"); // apply PHP's string increment/decrement rules + emitter.instruction("jmp __rt_mid_return_x86"); // share the epilogue with the numeric path + + emitter.label("__rt_mid_numeric_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // the delta becomes the right-hand operand of the numeric addition + emitter.instruction("xor esi, esi"); // integer payloads do not use a second word + emitter.instruction("xor eax, eax"); // runtime tag 0 = int + emitter.instruction("call __rt_mixed_from_value"); // box the delta so the shared numeric helper can consume it + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // keep the boxed delta so it can be released afterwards + emitter.instruction("mov rdi, rax"); // pass the boxed delta as the right-hand operand + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the borrowed operand cell as the left-hand operand + emitter.instruction("call __rt_mixed_numeric_add"); // reuse PHP's boxed numeric addition, including overflow promotion + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // preserve the boxed result while the delta temporary is released + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the boxed delta temporary + emitter.instruction("call __rt_decref_mixed"); // release the temporary so the increment does not leak one cell per use + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // restore the boxed result for the caller + + emitter.label("__rt_mid_return_x86"); + emitter.instruction("mov rsp, rbp"); // release every helper-local slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the boxed Mixed result in rax +} diff --git a/src/codegen_support/runtime/strings/str_ireplace.rs b/src/codegen_support/runtime/strings/str_ireplace.rs index 82c5719426..7a0e3317a8 100644 --- a/src/codegen_support/runtime/strings/str_ireplace.rs +++ b/src/codegen_support/runtime/strings/str_ireplace.rs @@ -7,6 +7,11 @@ //! //! Key details: //! - String helpers scan or transform byte ranges and return target ABI pointer/length pairs for generated call sites. +//! - The destination is sized before the first store: at most `subject_len / search_len` +//! replacements can fire, so `subject_len + (subject_len / search_len) * replacement_len` +//! bounds the result. That bound goes through `__rt_concat_reserve`, so an expanding +//! replacement falls back to heap storage instead of running off the end of the 64 KiB +//! concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -18,12 +23,14 @@ use crate::codegen_support::platform::Arch; /// # ABI /// - ARM64: search in x1/x2, replace in x3/x4, subject in x5/x6; result returned in x1/x2. /// - x86_64 Linux: search in rdi/rdx, replace in rsi/rcx, subject in r8/r9; result returned in rax/rdx. -/// Both variants write directly to the shared concat buffer and publish the updated write offset. +/// Both variants reserve their destination through `__rt_concat_reserve` and publish the written +/// length through `__rt_concat_publish`, which advances `_concat_off` only for scratch results. /// /// # Behavior /// Performs a single left-to-right pass over the subject string, replacing each /// case-insensitive occurrence of the search string with the replacement string. -/// Uses the concat buffer for output; the caller must ensure sufficient space is available. +/// The destination is sized up front, so oversized results use owned heap storage. +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. /// Returns the result pointer/length pair for the generated call site. pub fn emit_str_ireplace(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -42,13 +49,19 @@ pub fn emit_str_ireplace(emitter: &mut Emitter) { emitter.instruction("stp x3, x4, [sp, #16]"); // save replace ptr/len emitter.instruction("stp x5, x6, [sp, #32]"); // save subject ptr/len - // -- get concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // destination pointer + // -- reserve the bounded destination before the first store -- + emitter.instruction("mov x0, x6"); // an empty search never matches, so the subject length alone bounds the result + emitter.instruction("cbz x2, __rt_sirepl_reserve"); // skip the expansion arithmetic when the search string is empty + emitter.instruction("udiv x9, x6, x2"); // at most subject_len / search_len replacements can fire + emitter.instruction("umulh x10, x9, x4"); // capture the high half of the replacement-count * replacement-length product + emitter.instruction("cbnz x10, __rt_sirepl_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x9, x9, x4"); // total replacement bytes the loop can ever emit + emitter.instruction("adds x0, x6, x9"); // upper bound = subject length plus all emitted replacement bytes + emitter.instruction("b.cs __rt_sirepl_size_overflow"); // reject a wrapped bound instead of reserving a too-small destination + emitter.label("__rt_sirepl_reserve"); + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov x12, x0"); // destination pointer emitter.instruction("str x12, [sp, #48]"); // save result start - emitter.instruction("str x9, [sp, #56]"); // save offset variable ptr emitter.instruction("mov x13, #0"); // subject scan index emitter.label("__rt_sirepl_loop"); @@ -115,13 +128,14 @@ pub fn emit_str_ireplace(emitter: &mut Emitter) { emitter.label("__rt_sirepl_done"); emitter.instruction("ldr x1, [sp, #48]"); // result start emitter.instruction("sub x2, x12, x1"); // result length - emitter.instruction("ldr x9, [sp, #56]"); // offset variable ptr - emitter.instruction("ldr x10, [x9]"); // current offset - emitter.instruction("add x10, x10, x2"); // advance by result length - emitter.instruction("str x10, [x9]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame emitter.instruction("add sp, sp, #80"); // deallocate emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_sirepl_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of `__rt_str_ireplace` and its inner loop labels. @@ -150,12 +164,22 @@ fn emit_str_ireplace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // preserve the replacement string length across the replacement loop emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // preserve the subject string pointer across the replacement loop emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // preserve the subject string length across the replacement loop - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_off"); - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current concat-buffer write offset before emitting the replaced string - crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_concat_buf"); - emitter.instruction("lea r11, [r9 + r11]"); // compute the concat-buffer destination pointer where the replaced string begins + + // -- reserve the bounded destination before the first store -- + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the search-string length to decide how much expansion is possible + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // an empty search never matches, so the subject length alone bounds the result + emitter.instruction("test r10, r10"); // is the search string empty? + emitter.instruction("jz __rt_sirepl_reserve_linux_x86_64"); // skip the expansion arithmetic when the search string is empty + emitter.instruction("xor rdx, rdx"); // clear the high dividend word before the unsigned division + emitter.instruction("div r10"); // at most subject_len / search_len replacements can fire + emitter.instruction("imul rax, QWORD PTR [rbp - 32]"); // total replacement bytes the loop can ever emit + emitter.instruction("jo __rt_sirepl_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("add rax, QWORD PTR [rbp - 48]"); // upper bound = subject length plus all emitted replacement bytes + emitter.instruction("jc __rt_sirepl_size_overflow_linux_x86_64"); // reject a wrapped bound instead of reserving a too-small destination + emitter.label("__rt_sirepl_reserve_linux_x86_64"); + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov r11, rax"); // compute the destination pointer where the replaced string begins emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // preserve the replaced-string start pointer for the final string return pair - emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // preserve the concat-offset symbol address so the helper can publish the new write position emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // start scanning the subject string from byte offset zero emitter.label("__rt_sirepl_loop_linux_x86_64"); @@ -231,14 +255,15 @@ fn emit_str_ireplace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_sirepl_loop_linux_x86_64"); // continue scanning the subject string after copying the unmatched byte emitter.label("__rt_sirepl_done_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the concat-buffer start pointer of the replaced string in the primary x86_64 string result register - emitter.instruction("mov rdx, r11"); // copy the concat-buffer end pointer so the final replaced-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the concat-buffer start/end pointers - emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the concat-offset symbol address before publishing the new write position - emitter.instruction("mov r8, QWORD PTR [rcx]"); // reload the old concat-buffer write offset before advancing it by the replaced-string length - emitter.instruction("add r8, rdx"); // advance the concat-buffer write offset by the emitted replaced-string length - emitter.instruction("mov QWORD PTR [rcx], r8"); // publish the updated concat-buffer write offset after emitting the replaced string + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the reserved start pointer of the replaced string in the primary x86_64 string result register + emitter.instruction("mov rdx, r11"); // copy the destination end pointer so the final replaced-string length can be derived + emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the destination start/end pointers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 80"); // release the str_ireplace() spill slots before returning the replaced string emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the replaced string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_sirepl_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/str_pad.rs b/src/codegen_support/runtime/strings/str_pad.rs index 72a7efdfd9..811b982b6e 100644 --- a/src/codegen_support/runtime/strings/str_pad.rs +++ b/src/codegen_support/runtime/strings/str_pad.rs @@ -7,6 +7,10 @@ //! //! Key details: //! - String helpers scan or transform byte ranges and return target ABI pointer/length pairs for generated call sites. +//! - The padded width is reserved through `__rt_concat_reserve` before the first store, so a +//! target length larger than the remaining 64 KiB concat scratch takes owned heap storage +//! and an impossible target length (e.g. `PHP_INT_MAX`) is a controlled fatal error instead +//! of a multi-exabyte write loop. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -14,6 +18,8 @@ use crate::codegen_support::platform::Arch; /// str_pad: pad a string to a target length. /// Input: x1/x2=input, x3/x4=pad_str, x5=target_len, x7=pad_type (0=left, 1=right, 2=both). /// Output: x1/x2=result. +/// The destination comes from `__rt_concat_reserve` (concat scratch while the target width +/// fits, owned heap storage otherwise) and is finished through `__rt_concat_publish`. pub fn emit_str_pad(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_str_pad_linux_x86_64(emitter); @@ -35,12 +41,13 @@ pub fn emit_str_pad(emitter: &mut Emitter) { emitter.instruction("cmp x2, x5"); // compare input len with target emitter.instruction("b.ge __rt_str_pad_noop"); // already long enough → return copy - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // destination pointer + // -- reserve exactly the requested padded width before writing anything -- + emitter.instruction("mov x0, x5"); // the padded result is exactly the requested target width + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the padded result + emitter.instruction("mov x12, x0"); // destination pointer emitter.instruction("mov x13, x12"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the input string pointer and length after the reservation call + emitter.instruction("ldr x5, [sp, #32]"); // reload the requested target width after the reservation call emitter.instruction("sub x14, x5, x2"); // pad_needed = target - input_len emitter.instruction("ldr x7, [sp, #40]"); // reload pad_type @@ -69,16 +76,16 @@ pub fn emit_str_pad(emitter: &mut Emitter) { emitter.label("__rt_str_pad_emit"); // left padding emitter.instruction("mov x17, x15"); // left pad counter - emitter.instruction("mov x18, #0"); // pad string index + emitter.instruction("mov x9, #0"); // pad string index emitter.label("__rt_str_pad_lp"); emitter.instruction("cbz x17, __rt_str_pad_input"); // left padding done → copy input emitter.instruction("ldp x3, x4, [sp, #16]"); // reload pad string - emitter.instruction("ldrb w0, [x3, x18]"); // load pad char at index + emitter.instruction("ldrb w0, [x3, x9]"); // load pad char at index emitter.instruction("strb w0, [x12], #1"); // write to output emitter.instruction("sub x17, x17, #1"); // decrement left pad remaining - emitter.instruction("add x18, x18, #1"); // advance pad index - emitter.instruction("cmp x18, x4"); // wrap around if past pad string - emitter.instruction("csel x18, xzr, x18, ge"); // reset to 0 if >= pad_len + emitter.instruction("add x9, x9, #1"); // advance pad index + emitter.instruction("cmp x9, x4"); // wrap around if past pad string + emitter.instruction("csel x9, xzr, x9, ge"); // reset to 0 if >= pad_len emitter.instruction("b __rt_str_pad_lp"); // continue // copy input @@ -95,25 +102,22 @@ pub fn emit_str_pad(emitter: &mut Emitter) { // right padding emitter.label("__rt_str_pad_rp"); emitter.instruction("mov x17, x16"); // right pad counter - emitter.instruction("mov x18, #0"); // pad string index + emitter.instruction("mov x9, #0"); // pad string index emitter.label("__rt_str_pad_rp_loop"); emitter.instruction("cbz x17, __rt_str_pad_done"); // right padding done emitter.instruction("ldp x3, x4, [sp, #16]"); // reload pad string - emitter.instruction("ldrb w0, [x3, x18]"); // load pad char + emitter.instruction("ldrb w0, [x3, x9]"); // load pad char emitter.instruction("strb w0, [x12], #1"); // write to output emitter.instruction("sub x17, x17, #1"); // decrement - emitter.instruction("add x18, x18, #1"); // advance pad index - emitter.instruction("cmp x18, x4"); // wrap around - emitter.instruction("csel x18, xzr, x18, ge"); // reset to 0 + emitter.instruction("add x9, x9, #1"); // advance pad index + emitter.instruction("cmp x9, x4"); // wrap around + emitter.instruction("csel x9, xzr, x9, ge"); // reset to 0 emitter.instruction("b __rt_str_pad_rp_loop"); // continue emitter.label("__rt_str_pad_done"); emitter.instruction("mov x1, x13"); // result pointer emitter.instruction("sub x2, x12, x13"); // result length - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - emitter.instruction("add x10, x10, x2"); // advance by result length - emitter.instruction("str x10, [x9]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame emitter.instruction("add sp, sp, #64"); // deallocate emitter.instruction("ret"); // return @@ -129,7 +133,8 @@ pub fn emit_str_pad(emitter: &mut Emitter) { /// Uses the System V AMD64 ABI: rdi=input_ptr, rdx=input_len, rsi=pad_str_ptr, /// rcx=target_len, r8=pad_type (0=left, 1=right, 2=both). /// Output: rax=result_ptr, rdx=result_len. -/// Writes the padded result into the concat-buffer and advances `_concat_off`. +/// Writes the padded result into storage reserved by `__rt_concat_reserve` and finishes +/// through `__rt_concat_publish`, which advances `_concat_off` only for scratch results. fn emit_str_pad_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: str_pad ---"); @@ -145,12 +150,10 @@ fn emit_str_pad_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // preserve the requested pad type across the padding loops emitter.instruction("cmp rdx, rcx"); // does the input string already meet or exceed the requested target width? emitter.instruction("jge __rt_str_pad_noop_linux_x86_64"); // return a copied input string immediately when no padding is required - crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_concat_off"); - emitter.instruction("mov r10, QWORD PTR [r9]"); // load the current concat-buffer write offset before emitting the padded result - crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_concat_buf"); - emitter.instruction("lea r11, [r11 + r10]"); // compute the concat-buffer destination pointer where the padded result begins + emitter.instruction("mov rax, rcx"); // the padded result is exactly the requested target width + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the padded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the padded result begins emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // preserve the padded-result start pointer for the final string return pair - emitter.instruction("mov QWORD PTR [rbp - 64], r9"); // preserve the concat-offset symbol address so the helper can publish the final write offset emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the requested target length before computing the total number of pad bytes emitter.instruction("sub r10, QWORD PTR [rbp - 16]"); // compute how many pad bytes are needed to reach the requested target width emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the requested pad type before splitting the total pad budget @@ -228,13 +231,10 @@ fn emit_str_pad_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_str_pad_right_loop_linux_x86_64"); // continue emitting the remaining right-padding bytes emitter.label("__rt_str_pad_done_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the concat-buffer start pointer of the padded string in the primary x86_64 string result register - emitter.instruction("mov rdx, r11"); // copy the concat-buffer end pointer so the final padded-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the padded-string length from the concat-buffer start/end pointers - emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the concat-offset symbol address before publishing the new write position - emitter.instruction("mov r8, QWORD PTR [rcx]"); // reload the old concat-buffer write offset before advancing it by the padded-string length - emitter.instruction("add r8, rdx"); // advance the concat-buffer write offset by the emitted padded-string length - emitter.instruction("mov QWORD PTR [rcx], r8"); // publish the updated concat-buffer write offset after emitting the padded string + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the reserved start pointer of the padded string in the primary x86_64 string result register + emitter.instruction("mov rdx, r11"); // copy the destination end pointer so the final padded-string length can be derived + emitter.instruction("sub rdx, rax"); // derive the padded-string length from the destination start/end pointers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 96"); // release the str_pad() spill slots before returning the padded string emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the padded string in the standard x86_64 string result registers diff --git a/src/codegen_support/runtime/strings/str_persist.rs b/src/codegen_support/runtime/strings/str_persist.rs index 8c99e93048..54ca136deb 100644 --- a/src/codegen_support/runtime/strings/str_persist.rs +++ b/src/codegen_support/runtime/strings/str_persist.rs @@ -7,7 +7,11 @@ //! //! Key details: //! - String helpers scan or transform byte ranges and return target ABI pointer/length pairs for generated call sites. +//! - A source already carrying `CONCAT_TEMP_HEAP_KIND` is a heap-backed `.` operator temporary: +//! it is taken over in place (retagged as an owned string) instead of being duplicated, so a +//! `$s .= ...` accumulation loop does not leave one oversized block behind per append. +use crate::codegen_support::runtime::strings::concat_scratch::CONCAT_TEMP_HEAP_KIND; use crate::codegen_support::{emit::Emitter, platform::Arch}; @@ -15,6 +19,11 @@ use crate::codegen_support::{emit::Emitter, platform::Arch}; /// Used to persist strings that would otherwise outlive their current owner. /// Input: x1=ptr, x2=len /// Output: x1=new_ptr (on heap), x2=len (unchanged) +/// +/// A source stamped with `CONCAT_TEMP_HEAP_KIND` is an unowned `.` operator temporary with at +/// most one consumer, so ownership is transferred by retagging the existing block as heap kind 1 +/// instead of allocating and copying a second one. Every other source (rodata literals, concat +/// scratch slices, already-owned strings) still gets a fresh owned duplicate. pub fn emit_str_persist(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_str_persist_linux_x86_64(emitter); @@ -33,6 +42,19 @@ pub fn emit_str_persist(emitter: &mut Emitter) { emitter.instruction("cmp x1, x9"); // preserve the dedicated null-string sentinel across ownership stabilization emitter.instruction("b.eq __rt_str_persist_done"); // a missing string has no payload to allocate or copy + // -- take over a transient `.` operator temporary instead of duplicating it -- + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the frame pointer and return address across the heap-kind probe + emitter.instruction("mov x29, sp"); // establish the persist-helper probe frame pointer + emitter.instruction("mov x0, x1"); // pass the source payload pointer to the uniform heap-kind probe + emitter.instruction("bl __rt_heap_kind"); // classify the source storage without disturbing the x1/x2 string pair + emitter.instruction(&format!("cmp x0, #{}", CONCAT_TEMP_HEAP_KIND)); // is the source an unowned heap-backed concat temporary? + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the frame pointer and return address after the probe + emitter.instruction("b.ne __rt_str_persist_duplicate"); // every other source still gets a fresh owned duplicate + emitter.instruction("mov x9, #1"); // heap kind 1 = persisted elephc string + emitter.instruction("str x9, [x1, #-8]"); // retag the concat temporary as an owned string in place + emitter.instruction("ret"); // return the taken-over block with its length unchanged + emitter.label("__rt_str_persist_duplicate"); + // -- zero-length strings still get an owned heap block so callers never alias a borrowed source pointer -- // (the old early-return let explode()'s empty segment alias the subject string and double-free it on release) @@ -113,7 +135,21 @@ fn emit_str_persist_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 16"); // reserve local slots for the source pointer and source length across the allocator call emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source pointer across the heap allocation helper call emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the source length across the heap allocation helper call - emitter.instruction("mov rax, rdx"); // move the byte length into the x86_64 heap helper input register + + // -- take over a transient `.` operator temporary instead of duplicating it -- + emitter.instruction("call __rt_heap_kind"); // classify the source storage (rax already holds the source pointer) + emitter.instruction(&format!("cmp eax, {}", CONCAT_TEMP_HEAP_KIND)); // is the source an unowned heap-backed concat temporary? + emitter.instruction("jne __rt_str_persist_duplicate"); // every other source still gets a fresh owned duplicate + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the concat temporary payload pointer for the in-place retag + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 heap magic marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // retag the concat temporary as an owned string in place + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // restore the original string length for the x86_64 string result pair + emitter.instruction("add rsp, 16"); // release the temporary spill slots used by the persist helper + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning + emitter.instruction("ret"); // return the taken-over block with its length unchanged + + emitter.label("__rt_str_persist_duplicate"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the byte length into the x86_64 heap helper input register emitter.instruction("call __rt_heap_alloc"); // allocate owned string storage and return the payload pointer in rax emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 heap magic marker emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocated payload as a persisted elephc string in the uniform heap header diff --git a/src/codegen_support/runtime/strings/str_repeat.rs b/src/codegen_support/runtime/strings/str_repeat.rs index 415b52a79c..2559c57270 100644 --- a/src/codegen_support/runtime/strings/str_repeat.rs +++ b/src/codegen_support/runtime/strings/str_repeat.rs @@ -6,7 +6,11 @@ //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: -//! - Large repeated strings fall back to heap storage so they cannot overrun the fixed concat scratch buffer. +//! - `length * times` is computed with an overflow check (`umulh` / `imul`+`jo`); a wrapped +//! product reports PHP's allocation-overflow fatal instead of writing `times * length` +//! bytes into a destination sized by the wrapped value. +//! - Storage comes from `__rt_concat_reserve`, so large repeated strings fall back to heap +//! storage instead of overrunning the fixed 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -31,11 +35,12 @@ use crate::codegen_support::runtime::data::STR_REPEAT_TIMES_MSG; /// /// # Behavior /// - If `times == 0` or source length is 0, returns an empty string (null pointer, zero length). -/// - If the repeated result fits within concat scratch (64 KiB), writes directly there and -/// advances the concat scratch write offset. -/// - If the result exceeds concat scratch capacity, allocates a heap buffer, stamps it as -/// an owned string, and does NOT update concat scratch offset. +/// - Sizes the result through `__rt_concat_reserve`: concat scratch while it fits within the +/// 64 KiB buffer, an owned heap allocation otherwise. `__rt_concat_publish` then advances +/// the concat scratch offset only for scratch-backed results. /// - On negative repetition count, emits a fatal error message and terminates the process. +/// - When `length * times` overflows a machine word, branches to `__rt_alloc_overflow` and +/// terminates with PHP's "Possible integer overflow in memory allocation" fatal error. pub fn emit_str_repeat(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_str_repeat_linux_x86_64(emitter); @@ -46,10 +51,10 @@ pub fn emit_str_repeat(emitter: &mut Emitter) { emitter.comment("--- runtime: str_repeat ---"); emitter.label_global("__rt_str_repeat"); - // -- set up stack frame (80 bytes) -- - emitter.instruction("sub sp, sp, #80"); // allocate spill space for inputs, result metadata, and heap fallback state - emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #64"); // establish new frame pointer + // -- set up stack frame (64 bytes) -- + emitter.instruction("sub sp, sp, #64"); // allocate spill space for inputs and result metadata + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish new frame pointer emitter.instruction("stp x1, x2, [sp]"); // save source pointer and length emitter.instruction("str x3, [sp, #16]"); // save repetition count @@ -59,31 +64,15 @@ pub fn emit_str_repeat(emitter: &mut Emitter) { emitter.instruction("cbz x3, __rt_str_repeat_empty"); // return the canonical empty string when no repetitions are requested emitter.instruction("cbz x2, __rt_str_repeat_empty"); // return the canonical empty string when the source has no bytes - // -- choose concat scratch storage when the repeated result fits -- + // -- size the result, rejecting a wrapped length * times product -- + emitter.instruction("umulh x5, x2, x3"); // capture the high half of the length * repetition-count product + emitter.instruction("cbnz x5, __rt_str_repeat_size_overflow"); // a non-zero high half means the byte count cannot be represented emitter.instruction("mul x4, x2, x3"); // compute result length = source length * repetition count emitter.instruction("str x4, [sp, #24]"); // save result length for finalization - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current concat scratch write offset - emitter.instruction("add x5, x8, x4"); // compute concat scratch end offset after this append - emitter.instruction("mov x12, #65536"); // load concat scratch capacity in bytes - emitter.instruction("cmp x5, x12"); // does the repeated result fit in concat scratch storage? - emitter.instruction("b.hi __rt_str_repeat_heap"); // use heap fallback when concat scratch would overflow - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // compute concat scratch destination pointer - emitter.instruction("str x9, [sp, #32]"); // save result start pointer for the return pair - emitter.instruction("str xzr, [sp, #40]"); // mark result as concat-backed for final offset publication - emitter.instruction("b __rt_str_repeat_copy_start"); // skip heap allocation when scratch storage is enough - - // -- heap fallback for results that do not fit in concat scratch storage -- - emitter.label("__rt_str_repeat_heap"); - emitter.instruction("mov x0, x4"); // pass requested payload size to the heap allocator - emitter.instruction("bl __rt_heap_alloc"); // allocate owned storage for the repeated string payload - emitter.instruction("mov x6, #1"); // heap kind 1 = owned elephc string - emitter.instruction("str x6, [x0, #-8]"); // stamp the heap allocation as a string payload - emitter.instruction("mov x9, x0"); // initialize destination cursor at the heap payload start + emitter.instruction("mov x0, x4"); // request storage for the full repeated payload + emitter.instruction("bl __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the repeated string emitter.instruction("str x0, [sp, #32]"); // save result start pointer for the return pair - emitter.instruction("mov x13, #1"); // mark result as heap-backed so concat offset is left unchanged - emitter.instruction("str x13, [sp, #40]"); // save result storage kind for finalization + emitter.instruction("mov x9, x0"); // initialize the destination cursor at the reserved payload start // -- outer loop: repeat N times -- emitter.label("__rt_str_repeat_copy_start"); @@ -108,12 +97,7 @@ pub fn emit_str_repeat(emitter: &mut Emitter) { emitter.label("__rt_str_repeat_done"); emitter.instruction("ldr x1, [sp, #32]"); // return the repeated string pointer emitter.instruction("ldr x2, [sp, #24]"); // return the precomputed repeated string length - emitter.instruction("ldr x13, [sp, #40]"); // load storage kind: zero means concat-backed, one means heap-backed - emitter.instruction("cbnz x13, __rt_str_repeat_return"); // heap-backed results do not advance concat scratch offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // reload current concat scratch write offset - emitter.instruction("add x8, x8, x2"); // advance concat scratch offset by the repeated string length - emitter.instruction("str x8, [x6]"); // publish updated concat scratch offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("b __rt_str_repeat_return"); // skip the empty-string return setup // -- empty result: return null pointer with zero length -- @@ -123,8 +107,8 @@ pub fn emit_str_repeat(emitter: &mut Emitter) { // -- restore frame and return -- emitter.label("__rt_str_repeat_return"); - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #80"); // deallocate the repeat-helper stack frame + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // deallocate the repeat-helper stack frame emitter.instruction("ret"); // return to caller // -- fatal error: negative repetition count -- @@ -135,14 +119,18 @@ pub fn emit_str_repeat(emitter: &mut Emitter) { emitter.syscall(4); emitter.instruction("mov x0, #1"); // exit code 1 for the negative-repeat abort path emitter.syscall(1); + + // -- fatal error: length * times does not fit a machine word -- + emitter.label("__rt_str_repeat_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the `__rt_str_repeat` runtime helper for repeating a string N times on Linux x86_64. /// /// Uses the standard x86_64 System V ABI: source string in `rax/rdx`, repetition count in `rdi`. /// Result is returned in `rax/rdx` (pointer/length). Behavior mirrors the ARM64 variant: -/// concat scratch fallback when the result fits within 64 KiB, heap allocation otherwise, -/// fatal error on negative repetition count. +/// `__rt_concat_reserve` picks concat scratch or heap storage, `imul`+`jo` rejects a wrapped +/// `length * times` product, and a negative repetition count is a fatal error. fn emit_str_repeat_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: str_repeat ---"); @@ -162,31 +150,15 @@ fn emit_str_repeat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rdx, rdx"); // check whether the source string contains any bytes to repeat emitter.instruction("jz __rt_str_repeat_empty_linux_x86_64"); // return an empty string when the source payload is empty - // -- choose concat scratch storage when the repeated result fits -- + // -- size the result, rejecting a wrapped length * times product -- emitter.instruction("mov rcx, rdx"); // seed result length from the source string length emitter.instruction("imul rcx, rdi"); // compute result length = source length * repetition count + emitter.instruction("jo __rt_str_repeat_size_overflow_linux_x86_64"); // a wrapped product cannot describe the bytes the copy loop would write emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save result length for finalization - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load current concat scratch write offset - emitter.instruction("mov rdx, r9"); // copy the current offset before adding the requested result length - emitter.instruction("add rdx, rcx"); // compute concat scratch end offset after this append - emitter.instruction("cmp rdx, 65536"); // does the repeated result fit in concat scratch storage? - emitter.instruction("ja __rt_str_repeat_heap_linux_x86_64"); // use heap fallback when concat scratch would overflow - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute concat scratch destination pointer - emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // preserve the repeated-string start pointer for the return pair - emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // mark result as concat-backed for final offset publication - emitter.instruction("jmp __rt_str_repeat_copy_start_linux_x86_64"); // skip heap allocation when scratch storage is enough - - // -- heap fallback for results that do not fit in concat scratch storage -- - emitter.label("__rt_str_repeat_heap_linux_x86_64"); - emitter.instruction("mov rax, rcx"); // pass requested payload size to the heap allocator - emitter.instruction("call __rt_heap_alloc"); // allocate owned storage for the repeated string payload - emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(1))); // materialize the owned-string heap kind word with the x86_64 heap marker - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap allocation as a string payload + emitter.instruction("mov rax, rcx"); // request storage for the full repeated payload + emitter.instruction("call __rt_concat_reserve"); // reserve concat scratch or owned heap storage for the repeated string emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // preserve the repeated-string start pointer for the return pair - emitter.instruction("mov r11, rax"); // initialize the heap destination cursor at the result payload start - emitter.instruction("mov QWORD PTR [rbp - 48], 1"); // mark result as heap-backed so concat offset is left unchanged + emitter.instruction("mov r11, rax"); // initialize the destination cursor at the reserved payload start // -- outer loop: repeat N times -- emitter.label("__rt_str_repeat_copy_start_linux_x86_64"); @@ -216,13 +188,7 @@ fn emit_str_repeat_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_str_repeat_done_linux_x86_64"); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the repeated string pointer emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // return the precomputed repeated string length - emitter.instruction("mov r8, QWORD PTR [rbp - 48]"); // load storage kind: zero means concat-backed, one means heap-backed - emitter.instruction("test r8, r8"); // check whether the repeated result used heap fallback - emitter.instruction("jnz __rt_str_repeat_return_linux_x86_64"); // heap-backed results do not advance concat scratch offset - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // reload current concat scratch write offset - emitter.instruction("add r9, rdx"); // advance concat scratch offset by the repeated string length - emitter.instruction("mov QWORD PTR [r8], r9"); // publish updated concat scratch offset + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("jmp __rt_str_repeat_return_linux_x86_64"); // skip the empty-string return setup // -- empty result: return null pointer with zero length -- @@ -245,4 +211,8 @@ fn emit_str_repeat_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov edi, 1"); // exit code 1 for the negative-repeat abort path emitter.instruction("mov eax, 231"); // Linux x86_64 syscall 231 = exit_group emitter.instruction("syscall"); // terminate the process after reporting the invalid repeat count + + // -- fatal error: length * times does not fit a machine word -- + emitter.label("__rt_str_repeat_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/str_replace.rs b/src/codegen_support/runtime/strings/str_replace.rs index dd8217cb84..0ef67932ec 100644 --- a/src/codegen_support/runtime/strings/str_replace.rs +++ b/src/codegen_support/runtime/strings/str_replace.rs @@ -7,6 +7,11 @@ //! //! Key details: //! - String helpers scan or transform byte ranges and return target ABI pointer/length pairs for generated call sites. +//! - The destination is sized before the first store: at most `subject_len / search_len` +//! replacements can fire, so `subject_len + (subject_len / search_len) * replacement_len` +//! bounds the result. That bound goes through `__rt_concat_reserve`, so an expanding +//! replacement falls back to heap storage instead of running off the end of the 64 KiB +//! concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -22,11 +27,14 @@ use crate::codegen_support::platform::Arch; /// - `x5/x6`: subject string pointer and length /// /// # Output (ARM64 calling convention) -/// - `x1`: result string pointer in `_concat_buf` +/// - `x1`: result string pointer /// - `x2`: result string length /// /// # Side effects -/// - Advances the `_concat_off` global write offset by the result length. +/// - Reserves the bounded destination through `__rt_concat_reserve` and publishes the written +/// length through `__rt_concat_publish`, which advances `_concat_off` only for scratch-backed +/// results. Clobbers every caller-saved register, because the reservation can reach +/// `__rt_heap_alloc`. A wrapped size bound reports PHP's allocation-overflow fatal. /// /// # Dispatch /// On x86_64 Linux, delegates to `emit_str_replace_linux_x86_64`. @@ -50,13 +58,19 @@ pub fn emit_str_replace(emitter: &mut Emitter) { emitter.instruction("stp x3, x4, [sp, #16]"); // save replacement string ptr and length emitter.instruction("stp x5, x6, [sp, #32]"); // save subject string ptr and length - // -- get concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // compute destination pointer + // -- reserve the bounded destination before the first store -- + emitter.instruction("mov x0, x6"); // an empty search never matches, so the subject length alone bounds the result + emitter.instruction("cbz x2, __rt_str_replace_reserve"); // skip the expansion arithmetic when the search string is empty + emitter.instruction("udiv x9, x6, x2"); // at most subject_len / search_len replacements can fire + emitter.instruction("umulh x10, x9, x4"); // capture the high half of the replacement-count * replacement-length product + emitter.instruction("cbnz x10, __rt_str_replace_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x9, x9, x4"); // total replacement bytes the loop can ever emit + emitter.instruction("adds x0, x6, x9"); // upper bound = subject length plus all emitted replacement bytes + emitter.instruction("b.cs __rt_str_replace_size_overflow"); // reject a wrapped bound instead of reserving a too-small destination + emitter.label("__rt_str_replace_reserve"); + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov x12, x0"); // compute destination pointer emitter.instruction("str x12, [sp, #48]"); // save result start pointer - emitter.instruction("str x9, [sp, #56]"); // save offset variable address // -- initialize subject scan index -- emitter.instruction("mov x13, #0"); // subject index = 0 @@ -111,19 +125,20 @@ pub fn emit_str_replace(emitter: &mut Emitter) { emitter.instruction("add x13, x13, #1"); // advance subject index by 1 emitter.instruction("b __rt_str_replace_loop"); // continue scanning - // -- finalize: compute result length and update concat_off -- + // -- finalize: compute result length and publish the written bytes -- emitter.label("__rt_str_replace_done"); emitter.instruction("ldr x1, [sp, #48]"); // load result start pointer emitter.instruction("sub x2, x12, x1"); // result length = dest_end - dest_start - emitter.instruction("ldr x9, [sp, #56]"); // load offset variable address - emitter.instruction("ldr x10, [x9]"); // load current concat_off - emitter.instruction("add x10, x10, x2"); // advance offset by result length - emitter.instruction("str x10, [x9]"); // store updated concat_off + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results // -- restore frame and return -- emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #80"); // deallocate stack frame emitter.instruction("ret"); // return to caller + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_str_replace_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of the `__rt_str_replace` runtime helper. @@ -156,12 +171,22 @@ fn emit_str_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // preserve the replacement string length across the replacement loop emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // preserve the subject string pointer across the replacement loop emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // preserve the subject string length across the replacement loop - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_off"); - emitter.instruction("mov r11, QWORD PTR [r10]"); // load the current concat-buffer write offset before emitting the replaced string - crate::codegen_support::abi::emit_symbol_address(emitter, "r9", "_concat_buf"); - emitter.instruction("lea r11, [r9 + r11]"); // compute the concat-buffer destination pointer where the replaced string begins + + // -- reserve the bounded destination before the first store -- + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the search-string length to decide how much expansion is possible + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // an empty search never matches, so the subject length alone bounds the result + emitter.instruction("test r10, r10"); // is the search string empty? + emitter.instruction("jz __rt_str_replace_reserve_linux_x86_64"); // skip the expansion arithmetic when the search string is empty + emitter.instruction("xor rdx, rdx"); // clear the high dividend word before the unsigned division + emitter.instruction("div r10"); // at most subject_len / search_len replacements can fire + emitter.instruction("imul rax, QWORD PTR [rbp - 32]"); // total replacement bytes the loop can ever emit + emitter.instruction("jo __rt_str_replace_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("add rax, QWORD PTR [rbp - 48]"); // upper bound = subject length plus all emitted replacement bytes + emitter.instruction("jc __rt_str_replace_size_overflow_linux_x86_64"); // reject a wrapped bound instead of reserving a too-small destination + emitter.label("__rt_str_replace_reserve_linux_x86_64"); + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov r11, rax"); // compute the destination pointer where the replaced string begins emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // preserve the replaced-string start pointer for the final string return pair - emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // preserve the concat-offset symbol address so the helper can publish the new write position emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // start scanning the subject string from byte offset zero emitter.label("__rt_str_replace_loop_linux_x86_64"); @@ -223,14 +248,15 @@ fn emit_str_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_str_replace_loop_linux_x86_64"); // continue scanning the subject string after copying the unmatched byte emitter.label("__rt_str_replace_done_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the concat-buffer start pointer of the replaced string in the primary x86_64 string result register - emitter.instruction("mov rdx, r11"); // copy the concat-buffer end pointer so the final replaced-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the concat-buffer start/end pointers - emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the concat-offset symbol address before publishing the new write position - emitter.instruction("mov r8, QWORD PTR [rcx]"); // reload the old concat-buffer write offset before advancing it by the replaced-string length - emitter.instruction("add r8, rdx"); // advance the concat-buffer write offset by the emitted replaced-string length - emitter.instruction("mov QWORD PTR [rcx], r8"); // publish the updated concat-buffer write offset after emitting the replaced string + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the reserved start pointer of the replaced string in the primary x86_64 string result register + emitter.instruction("mov rdx, r11"); // copy the destination end pointer so the final replaced-string length can be derived + emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the destination start/end pointers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 80"); // release the str_replace() spill slots before returning the replaced string emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the replaced string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_str_replace_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/str_to_int.rs b/src/codegen_support/runtime/strings/str_to_int.rs index 7234729066..55ea6d72c9 100644 --- a/src/codegen_support/runtime/strings/str_to_int.rs +++ b/src/codegen_support/runtime/strings/str_to_int.rs @@ -7,6 +7,9 @@ //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: +//! - `__rt_php_num_scan` clips the scratch string to PHP's leading numeric run before either +//! libc parser runs, so `"0x1A"` is `0` (not `26`), `"INF"`/`"NAN"` are `0`, and `"1_000"` +//! is `1` — libc's `strtoll`/`strtod` extensions never reach the value. //! - `strtoll` gives the exact 64-bit value and PHP's saturating overflow (LLONG_MAX/MIN == PHP_INT_MAX/MIN), //! so large integer strings are not rounded through `f64`. //! - When `strtod` consumes more bytes than `strtoll`, the string has a `.`/`e` float part (e.g. `"1e3"`), @@ -18,9 +21,10 @@ use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; /// /// Input follows the active string-result convention: /// AArch64 uses `x1`/`x2`; x86_64 uses `rax`/`rdx`. -/// The helper copies the string into the C-string scratch buffer via `__rt_cstr`, then parses it -/// with `strtoll` (exact + saturating) and `strtod`, returning the integer-form value unless the -/// string is float-form, in which case the truncated double is returned. +/// The helper copies the string into the C-string scratch buffer via `__rt_cstr`, clips it to PHP's +/// leading numeric run with `__rt_php_num_scan`, then parses that run with `strtoll` (exact + +/// saturating) and `strtod`, returning the integer-form value unless the run is float-form, in +/// which case the truncated double is returned. pub fn emit_str_to_int(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_str_to_int_linux_x86_64(emitter); @@ -36,18 +40,19 @@ pub fn emit_str_to_int(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across the libc calls emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer - // -- copy the PHP string into the C-string scratch buffer -- + // -- copy the PHP string into the C-string scratch buffer and clip it to PHP's grammar -- emitter.instruction("bl __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("str x0, [sp, #24]"); // save the C-string pointer for the second parse + emitter.instruction("bl __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("str x0, [sp, #24]"); // save the clipped run pointer for the second parse - // -- integer parse: strtoll(cstr, &end_i, 10) gives the exact, saturating 64-bit value -- + // -- integer parse: strtoll(run, &end_i, 10) gives the exact, saturating 64-bit value -- emitter.instruction("add x1, sp, #0"); // pass &end_i so strtoll reports where the integer prefix ended emitter.instruction("mov x2, #10"); // parse in base 10 like PHP string-to-int emitter.bl_c("strtoll"); emitter.instruction("str x0, [sp, #16]"); // save the integer-form value (LLONG_MAX/MIN on overflow == PHP_INT_MAX/MIN) // -- float parse: strtod(cstr, &end_d) detects a '.'/'e' float continuation -- - emitter.instruction("ldr x0, [sp, #24]"); // reload the C-string pointer for strtod + emitter.instruction("ldr x0, [sp, #24]"); // reload the clipped run pointer for strtod emitter.instruction("add x1, sp, #8"); // pass &end_d so strtod reports where the numeric value ended emitter.bl_c("strtod"); @@ -71,8 +76,9 @@ pub fn emit_str_to_int(emitter: &mut Emitter) { /// Emits the Linux x86_64 `__rt_str_to_int` runtime helper. /// /// The input string arrives in the elephc string-result registers (`rax`/`rdx`). -/// Parses with `strtoll` (exact + saturating) and `strtod`, returning the integer-form value in -/// `rax` unless `strtod` consumed a `.`/`e` float part, in which case the truncated double is used. +/// Clips the scratch to PHP's leading numeric run with `__rt_php_num_scan`, then parses with +/// `strtoll` (exact + saturating) and `strtod`, returning the integer-form value in `rax` unless +/// `strtod` consumed a `.`/`e` float part, in which case the truncated double is used. fn emit_str_to_int_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: str_to_int ---"); @@ -83,19 +89,21 @@ fn emit_str_to_int_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 48"); // allocate aligned slots for the C-string pointer, integer value, and end pointers - // -- copy the PHP string into the C-string scratch buffer -- + // -- copy the PHP string into the C-string scratch buffer and clip it to PHP's grammar -- emitter.instruction("call __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the C-string pointer for the second parse + emitter.instruction("mov rdi, rax"); // pass the C-string pointer to the numeric-grammar scanner + emitter.instruction("call __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the clipped run pointer for the second parse - // -- integer parse: strtoll(cstr, &end_i, 10) gives the exact, saturating 64-bit value -- - emitter.instruction("mov rdi, rax"); // strtoll arg1: the C-string pointer + // -- integer parse: strtoll(run, &end_i, 10) gives the exact, saturating 64-bit value -- + emitter.instruction("mov rdi, rax"); // strtoll arg1: the clipped run pointer emitter.instruction("lea rsi, [rbp - 24]"); // strtoll arg2: &end_i emitter.instruction("mov edx, 10"); // strtoll arg3: parse in base 10 like PHP string-to-int emitter.instruction("call strtoll"); // rax = integer-form value (LLONG_MAX/MIN on overflow == PHP_INT_MAX/MIN) emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the integer-form value // -- float parse: strtod(cstr, &end_d) detects a '.'/'e' float continuation -- - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C-string pointer for strtod + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the clipped run pointer for strtod emitter.instruction("lea rsi, [rbp - 32]"); // strtod arg2: &end_d emitter.instruction("call strtod"); // xmm0 = parsed double value diff --git a/src/codegen_support/runtime/strings/str_to_int_base.rs b/src/codegen_support/runtime/strings/str_to_int_base.rs new file mode 100644 index 0000000000..52e7983c9e --- /dev/null +++ b/src/codegen_support/runtime/strings/str_to_int_base.rs @@ -0,0 +1,350 @@ +//! Purpose: +//! Emits the `__rt_str_to_int_base` runtime helper: PHP `intval()`'s two-argument string +//! parser, which is C `strtol()` plus php-src's extra `0b` binary prefix. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - php-src's `PHP_FUNCTION(intval)` special-cases a `0b`/`0B` prefix for bases `0` and `2` +//! and then hands everything else to `ZEND_STRTOL`, so this helper reproduces `strtol()`: +//! leading whitespace is skipped, one optional sign is consumed, a `0x`/`0X` prefix is +//! accepted for bases `0` and `16`, base `0` falls back to octal on a leading `0` and to +//! decimal otherwise, and the scan stops at the first byte that is not a digit of the +//! resolved base. That "stops at the first bad byte" rule is what separates this helper +//! from `__rt_base_to_number`, which instead *ignores* such bytes for `hexdec()` and +//! friends (`intval("a0z", 16) === 160` there, `2575` here would be wrong). +//! - A base that is neither `0` nor in `2..=36` makes `strtol()` fail with `EINVAL` and +//! return `0`; reference PHP 8.4 surfaces that as `intval("42", 1) === 0` with no +//! diagnostic, so the helper simply returns `0` instead of raising anything. +//! - Overflow saturates at `PHP_INT_MAX`/`PHP_INT_MIN` exactly like `ZEND_STRTOL`'s +//! `ERANGE` clamp (`intval("ffffffffffffffffff", 16) === PHP_INT_MAX`). The accumulator is +//! therefore unsigned and every comparison against it uses unsigned conditions, because a +//! negative parse legitimately reaches `2**63`. +//! - The helper allocates nothing and calls nothing, so it needs no frame. + +use crate::codegen_support::abi; +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_str_to_int_base` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = string pointer, `x2` = string length, `x3` = requested base. +/// Output: `x0` = the parsed PHP integer. +/// +/// ABI (x86_64 System V): +/// Input: `rdi` = string pointer, `rsi` = string length, `rdx` = requested base. +/// Output: `rax` = the parsed PHP integer. +/// +/// An empty string, a string whose first non-blank byte is not a digit of the resolved base, +/// and an out-of-range base all yield `0` — the same as reference PHP. +pub fn emit_str_to_int_base(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_str_to_int_base_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: str_to_int_base ---"); + emitter.label_global("__rt_str_to_int_base"); + + // -- reject the bases strtol() answers with EINVAL -- + emitter.instruction("cbz x3, __rt_str_to_int_base_scan"); // base 0 asks for prefix auto-detection + emitter.instruction("cmp x3, #2"); // is the requested base below strtol()'s minimum? + emitter.instruction("b.lt __rt_str_to_int_base_zero"); // an unusable base parses nothing + emitter.instruction("cmp x3, #36"); // is the requested base above strtol()'s maximum? + emitter.instruction("b.gt __rt_str_to_int_base_zero"); // an unusable base parses nothing + + // -- skip the leading whitespace strtol() ignores -- + emitter.label("__rt_str_to_int_base_scan"); + emitter.instruction("mov x4, #0"); // start with a positive sign + emitter.label("__rt_str_to_int_base_space"); + emitter.instruction("cbz x2, __rt_str_to_int_base_zero"); // a blank-only string parses nothing + emitter.instruction("ldrb w7, [x1]"); // load the next candidate byte without consuming it + emitter.instruction("cmp w7, #32"); // is the byte a plain space? + emitter.instruction("b.eq __rt_str_to_int_base_space_next"); // spaces are skipped + emitter.instruction("sub w9, w7, #9"); // rebase the byte onto the tab..carriage-return block + emitter.instruction("cmp w9, #4"); // is the byte one of \t \n \v \f \r? + emitter.instruction("b.hi __rt_str_to_int_base_sign"); // the first non-blank byte starts the number + emitter.label("__rt_str_to_int_base_space_next"); + emitter.instruction("add x1, x1, #1"); // consume the blank byte + emitter.instruction("sub x2, x2, #1"); // record that one input byte has been consumed + emitter.instruction("b __rt_str_to_int_base_space"); // keep skipping blanks + + // -- consume the single optional sign -- + emitter.label("__rt_str_to_int_base_sign"); + emitter.instruction("cmp w7, #45"); // is the byte a minus sign? + emitter.instruction("b.ne __rt_str_to_int_base_plus"); // try the plus sign instead + emitter.instruction("mov x4, #1"); // remember that the result is negative + emitter.instruction("b __rt_str_to_int_base_sign_taken"); // consume the sign byte + emitter.label("__rt_str_to_int_base_plus"); + emitter.instruction("cmp w7, #43"); // is the byte a plus sign? + emitter.instruction("b.ne __rt_str_to_int_base_prefix"); // no sign means the digits start here + emitter.label("__rt_str_to_int_base_sign_taken"); + emitter.instruction("add x1, x1, #1"); // consume the sign byte + emitter.instruction("sub x2, x2, #1"); // record that one input byte has been consumed + + // -- accept the 0x/0b prefixes the resolved base allows -- + emitter.label("__rt_str_to_int_base_prefix"); + emitter.instruction("cbz x2, __rt_str_to_int_base_zero"); // a sign with no digits parses nothing + emitter.instruction("cmp x2, #2"); // is the remainder long enough to carry a prefix? + emitter.instruction("b.lt __rt_str_to_int_base_auto"); // a single byte can only pick the automatic base + emitter.instruction("ldrb w7, [x1]"); // load the candidate prefix's leading zero + emitter.instruction("cmp w7, #48"); // does the remainder start with '0'? + emitter.instruction("b.ne __rt_str_to_int_base_auto"); // without a leading zero there is no prefix + emitter.instruction("ldrb w9, [x1, #1]"); // load the candidate prefix letter + emitter.instruction("orr w9, w9, #32"); // fold the prefix letter to lowercase + emitter.instruction("cmp w9, #120"); // is the prefix letter 'x'? + emitter.instruction("b.ne __rt_str_to_int_base_binary_prefix"); // try the binary prefix instead + emitter.instruction("cbz x3, __rt_str_to_int_base_take_hex"); // base 0 resolves '0x' to hexadecimal + emitter.instruction("cmp x3, #16"); // was hexadecimal requested explicitly? + emitter.instruction("b.ne __rt_str_to_int_base_auto"); // any other base treats 'x' as a terminator + emitter.label("__rt_str_to_int_base_take_hex"); + emitter.instruction("mov x3, #16"); // resolve the scan to base 16 + emitter.instruction("b __rt_str_to_int_base_skip_prefix"); // consume the two prefix bytes + emitter.label("__rt_str_to_int_base_binary_prefix"); + emitter.instruction("cmp w9, #98"); // is the prefix letter 'b'? + emitter.instruction("b.ne __rt_str_to_int_base_auto"); // any other letter is not a prefix + emitter.instruction("cbz x3, __rt_str_to_int_base_take_binary"); // base 0 resolves '0b' to binary + emitter.instruction("cmp x3, #2"); // was binary requested explicitly? + emitter.instruction("b.ne __rt_str_to_int_base_auto"); // any other base treats 'b' as a digit or terminator + emitter.label("__rt_str_to_int_base_take_binary"); + emitter.instruction("mov x3, #2"); // resolve the scan to base 2 + emitter.label("__rt_str_to_int_base_skip_prefix"); + emitter.instruction("add x1, x1, #2"); // consume the two prefix bytes + emitter.instruction("sub x2, x2, #2"); // record that the prefix has been consumed + emitter.instruction("b __rt_str_to_int_base_ready"); // the prefix already resolved the base + + // -- resolve base 0 the way strtol() does when no 0x/0b prefix applied -- + emitter.label("__rt_str_to_int_base_auto"); + emitter.instruction("cbnz x3, __rt_str_to_int_base_ready"); // an explicit base needs no auto-detection + emitter.instruction("ldrb w7, [x1]"); // inspect the first digit byte + emitter.instruction("mov x3, #10"); // default the automatic base to decimal + emitter.instruction("cmp w7, #48"); // does the number start with '0'? + emitter.instruction("b.ne __rt_str_to_int_base_ready"); // a non-zero lead digit keeps the decimal base + emitter.instruction("mov x3, #8"); // a leading zero selects octal, and stays a valid digit + + // -- derive the saturation limit this sign allows -- + emitter.label("__rt_str_to_int_base_ready"); + emitter.instruction("cbz x2, __rt_str_to_int_base_zero"); // a prefix with no digits parses nothing + abi::emit_load_int_immediate(emitter, "x6", i64::MAX); + emitter.instruction("cbz x4, __rt_str_to_int_base_limit_done"); // a positive parse saturates at PHP_INT_MAX + abi::emit_load_int_immediate(emitter, "x6", i64::MIN); + emitter.label("__rt_str_to_int_base_limit_done"); + emitter.instruction("udiv x10, x6, x3"); // x10 = limit / base, the last accumulator that can still take a digit + emitter.instruction("mov x5, #0"); // start the unsigned accumulator at zero + + // -- accumulate digits until the first byte that is not one -- + emitter.label("__rt_str_to_int_base_loop"); + emitter.instruction("cbz x2, __rt_str_to_int_base_done"); // stop once every input byte has been consumed + emitter.instruction("ldrb w7, [x1], #1"); // load the next input byte and advance the cursor + emitter.instruction("sub x2, x2, #1"); // record that one input byte has been consumed + emitter.instruction("sub w8, w7, #48"); // try the ASCII numerals first + emitter.instruction("cmp w8, #9"); // is the byte in '0'..'9' (unsigned, so lower bytes wrap high)? + emitter.instruction("b.ls __rt_str_to_int_base_digit"); // an ASCII numeral decodes directly + emitter.instruction("cmp w7, #65"); // is the byte below 'A'? + emitter.instruction("b.lo __rt_str_to_int_base_done"); // a non-digit byte terminates the scan + emitter.instruction("cmp w7, #90"); // is the byte at or below 'Z'? + emitter.instruction("b.hi __rt_str_to_int_base_lower"); // try the lowercase letters instead + emitter.instruction("sub w8, w7, #55"); // map 'A'..'Z' to digit values 10..35 + emitter.instruction("b __rt_str_to_int_base_digit"); // the uppercase letter decoded to a digit + emitter.label("__rt_str_to_int_base_lower"); + emitter.instruction("cmp w7, #97"); // is the byte below 'a'? + emitter.instruction("b.lo __rt_str_to_int_base_done"); // a non-digit byte terminates the scan + emitter.instruction("cmp w7, #122"); // is the byte above 'z'? + emitter.instruction("b.hi __rt_str_to_int_base_done"); // a non-digit byte terminates the scan + emitter.instruction("sub w8, w7, #87"); // map 'a'..'z' to digit values 10..35 + + emitter.label("__rt_str_to_int_base_digit"); + emitter.instruction("cmp x8, x3"); // is the decoded digit valid in the resolved base? + emitter.instruction("b.hs __rt_str_to_int_base_done"); // a digit outside the base terminates the scan + emitter.instruction("cmp x5, x10"); // would shifting the accumulator already pass the limit? + emitter.instruction("b.hi __rt_str_to_int_base_saturate"); // an accumulator past the threshold can only overflow + emitter.instruction("mul x5, x5, x3"); // shift the accumulator up by one digit position + emitter.instruction("sub x9, x6, x8"); // compute the largest accumulator this digit still fits in + emitter.instruction("cmp x5, x9"); // would adding this digit pass the limit? + emitter.instruction("b.hi __rt_str_to_int_base_saturate"); // clamp instead of wrapping past PHP_INT_MAX/PHP_INT_MIN + emitter.instruction("add x5, x5, x8"); // accumulate the digit into the unsigned result + emitter.instruction("b __rt_str_to_int_base_loop"); // continue scanning the remaining input bytes + + emitter.label("__rt_str_to_int_base_saturate"); + emitter.instruction("mov x5, x6"); // clamp to the limit this sign allows + + emitter.label("__rt_str_to_int_base_done"); + emitter.instruction("mov x0, x5"); // return the accumulated magnitude + emitter.instruction("cbz x4, __rt_str_to_int_base_ret"); // a positive parse is already the result + emitter.instruction("neg x0, x0"); // apply the consumed minus sign + emitter.label("__rt_str_to_int_base_ret"); + emitter.instruction("ret"); // hand the parsed integer back to the caller + + emitter.label("__rt_str_to_int_base_zero"); + emitter.instruction("mov x0, #0"); // an unusable base or digit-free string parses to zero + emitter.instruction("ret"); // hand the zero result back to the caller +} + +/// Emits `__rt_str_to_int_base` for x86_64 Linux using the System V ABI. +/// +/// The base is moved into `rcx` up front so `rdx` stays free for the decoded digit, and the +/// saturation threshold is derived with an unsigned division whose `rdx:rax` pair would +/// otherwise collide with the argument registers. +fn emit_str_to_int_base_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: str_to_int_base ---"); + emitter.label_global("__rt_str_to_int_base"); + + // -- reject the bases strtol() answers with EINVAL -- + emitter.instruction("mov rcx, rdx"); // keep the requested base in a stable register + emitter.instruction("test rcx, rcx"); // was base 0 requested? + emitter.instruction("jz __rt_str_to_int_base_scan_linux_x86_64"); // base 0 asks for prefix auto-detection + emitter.instruction("cmp rcx, 2"); // is the requested base below strtol()'s minimum? + emitter.instruction("jl __rt_str_to_int_base_zero_linux_x86_64"); // an unusable base parses nothing + emitter.instruction("cmp rcx, 36"); // is the requested base above strtol()'s maximum? + emitter.instruction("jg __rt_str_to_int_base_zero_linux_x86_64"); // an unusable base parses nothing + + // -- skip the leading whitespace strtol() ignores -- + emitter.label("__rt_str_to_int_base_scan_linux_x86_64"); + emitter.instruction("xor r8d, r8d"); // start with a positive sign + emitter.label("__rt_str_to_int_base_space_linux_x86_64"); + emitter.instruction("test rsi, rsi"); // is any input byte left? + emitter.instruction("jz __rt_str_to_int_base_zero_linux_x86_64"); // a blank-only string parses nothing + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // load the next candidate byte without consuming it + emitter.instruction("cmp eax, 32"); // is the byte a plain space? + emitter.instruction("je __rt_str_to_int_base_space_next_linux_x86_64"); // spaces are skipped + emitter.instruction("mov edx, eax"); // copy the byte before rebasing it + emitter.instruction("sub edx, 9"); // rebase the byte onto the tab..carriage-return block + emitter.instruction("cmp edx, 4"); // is the byte one of \t \n \v \f \r? + emitter.instruction("ja __rt_str_to_int_base_sign_linux_x86_64"); // the first non-blank byte starts the number + emitter.label("__rt_str_to_int_base_space_next_linux_x86_64"); + emitter.instruction("add rdi, 1"); // consume the blank byte + emitter.instruction("sub rsi, 1"); // record that one input byte has been consumed + emitter.instruction("jmp __rt_str_to_int_base_space_linux_x86_64"); // keep skipping blanks + + // -- consume the single optional sign -- + emitter.label("__rt_str_to_int_base_sign_linux_x86_64"); + emitter.instruction("cmp eax, 45"); // is the byte a minus sign? + emitter.instruction("jne __rt_str_to_int_base_plus_linux_x86_64"); // try the plus sign instead + emitter.instruction("mov r8, 1"); // remember that the result is negative + emitter.instruction("jmp __rt_str_to_int_base_sign_taken_linux_x86_64"); // consume the sign byte + emitter.label("__rt_str_to_int_base_plus_linux_x86_64"); + emitter.instruction("cmp eax, 43"); // is the byte a plus sign? + emitter.instruction("jne __rt_str_to_int_base_prefix_linux_x86_64"); // no sign means the digits start here + emitter.label("__rt_str_to_int_base_sign_taken_linux_x86_64"); + emitter.instruction("add rdi, 1"); // consume the sign byte + emitter.instruction("sub rsi, 1"); // record that one input byte has been consumed + + // -- accept the 0x/0b prefixes the resolved base allows -- + emitter.label("__rt_str_to_int_base_prefix_linux_x86_64"); + emitter.instruction("test rsi, rsi"); // is any input byte left? + emitter.instruction("jz __rt_str_to_int_base_zero_linux_x86_64"); // a sign with no digits parses nothing + emitter.instruction("cmp rsi, 2"); // is the remainder long enough to carry a prefix? + emitter.instruction("jl __rt_str_to_int_base_auto_linux_x86_64"); // a single byte can only pick the automatic base + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // load the candidate prefix's leading zero + emitter.instruction("cmp eax, 48"); // does the remainder start with '0'? + emitter.instruction("jne __rt_str_to_int_base_auto_linux_x86_64"); // without a leading zero there is no prefix + emitter.instruction("movzx edx, BYTE PTR [rdi + 1]"); // load the candidate prefix letter + emitter.instruction("or edx, 32"); // fold the prefix letter to lowercase + emitter.instruction("cmp edx, 120"); // is the prefix letter 'x'? + emitter.instruction("jne __rt_str_to_int_base_binary_prefix_linux_x86_64"); // try the binary prefix instead + emitter.instruction("test rcx, rcx"); // was base 0 requested? + emitter.instruction("jz __rt_str_to_int_base_take_hex_linux_x86_64"); // base 0 resolves '0x' to hexadecimal + emitter.instruction("cmp rcx, 16"); // was hexadecimal requested explicitly? + emitter.instruction("jne __rt_str_to_int_base_auto_linux_x86_64"); // any other base treats 'x' as a terminator + emitter.label("__rt_str_to_int_base_take_hex_linux_x86_64"); + emitter.instruction("mov rcx, 16"); // resolve the scan to base 16 + emitter.instruction("jmp __rt_str_to_int_base_skip_prefix_linux_x86_64"); // consume the two prefix bytes + emitter.label("__rt_str_to_int_base_binary_prefix_linux_x86_64"); + emitter.instruction("cmp edx, 98"); // is the prefix letter 'b'? + emitter.instruction("jne __rt_str_to_int_base_auto_linux_x86_64"); // any other letter is not a prefix + emitter.instruction("test rcx, rcx"); // was base 0 requested? + emitter.instruction("jz __rt_str_to_int_base_take_binary_linux_x86_64"); // base 0 resolves '0b' to binary + emitter.instruction("cmp rcx, 2"); // was binary requested explicitly? + emitter.instruction("jne __rt_str_to_int_base_auto_linux_x86_64"); // any other base treats 'b' as a digit or terminator + emitter.label("__rt_str_to_int_base_take_binary_linux_x86_64"); + emitter.instruction("mov rcx, 2"); // resolve the scan to base 2 + emitter.label("__rt_str_to_int_base_skip_prefix_linux_x86_64"); + emitter.instruction("add rdi, 2"); // consume the two prefix bytes + emitter.instruction("sub rsi, 2"); // record that the prefix has been consumed + emitter.instruction("jmp __rt_str_to_int_base_ready_linux_x86_64"); // the prefix already resolved the base + + // -- resolve base 0 the way strtol() does when no 0x/0b prefix applied -- + emitter.label("__rt_str_to_int_base_auto_linux_x86_64"); + emitter.instruction("test rcx, rcx"); // was an explicit base requested? + emitter.instruction("jnz __rt_str_to_int_base_ready_linux_x86_64"); // an explicit base needs no auto-detection + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // inspect the first digit byte + emitter.instruction("mov rcx, 10"); // default the automatic base to decimal + emitter.instruction("cmp eax, 48"); // does the number start with '0'? + emitter.instruction("jne __rt_str_to_int_base_ready_linux_x86_64"); // a non-zero lead digit keeps the decimal base + emitter.instruction("mov rcx, 8"); // a leading zero selects octal, and stays a valid digit + + // -- derive the saturation limit this sign allows -- + emitter.label("__rt_str_to_int_base_ready_linux_x86_64"); + emitter.instruction("test rsi, rsi"); // is any input byte left? + emitter.instruction("jz __rt_str_to_int_base_zero_linux_x86_64"); // a prefix with no digits parses nothing + abi::emit_load_int_immediate(emitter, "r10", i64::MAX); + emitter.instruction("test r8, r8"); // did the scan consume a minus sign? + emitter.instruction("jz __rt_str_to_int_base_limit_done_linux_x86_64"); // a positive parse saturates at PHP_INT_MAX + abi::emit_load_int_immediate(emitter, "r10", i64::MIN); + emitter.label("__rt_str_to_int_base_limit_done_linux_x86_64"); + emitter.instruction("mov rax, r10"); // stage the limit as the unsigned dividend + emitter.instruction("xor edx, edx"); // clear the high dividend half before the unsigned division + emitter.instruction("div rcx"); // rax = limit / base, the last accumulator that can still take a digit + emitter.instruction("mov r11, rax"); // keep the accumulator threshold for the overflow test + emitter.instruction("xor r9d, r9d"); // start the unsigned accumulator at zero + + // -- accumulate digits until the first byte that is not one -- + emitter.label("__rt_str_to_int_base_loop_linux_x86_64"); + emitter.instruction("test rsi, rsi"); // is any input byte left? + emitter.instruction("jz __rt_str_to_int_base_done_linux_x86_64"); // stop once every input byte has been consumed + emitter.instruction("movzx eax, BYTE PTR [rdi]"); // load the next input byte + emitter.instruction("add rdi, 1"); // advance the input cursor + emitter.instruction("sub rsi, 1"); // record that one input byte has been consumed + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its numeral value + emitter.instruction("sub rdx, 48"); // try the ASCII numerals first + emitter.instruction("cmp rdx, 9"); // is the byte in '0'..'9' (unsigned, so lower bytes wrap high)? + emitter.instruction("jbe __rt_str_to_int_base_digit_linux_x86_64"); // an ASCII numeral decodes directly + emitter.instruction("cmp rax, 65"); // is the byte below 'A'? + emitter.instruction("jb __rt_str_to_int_base_done_linux_x86_64"); // a non-digit byte terminates the scan + emitter.instruction("cmp rax, 90"); // is the byte above 'Z'? + emitter.instruction("ja __rt_str_to_int_base_lower_linux_x86_64"); // try the lowercase letters instead + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its letter value + emitter.instruction("sub rdx, 55"); // map 'A'..'Z' to digit values 10..35 + emitter.instruction("jmp __rt_str_to_int_base_digit_linux_x86_64"); // the uppercase letter decoded to a digit + emitter.label("__rt_str_to_int_base_lower_linux_x86_64"); + emitter.instruction("cmp rax, 97"); // is the byte below 'a'? + emitter.instruction("jb __rt_str_to_int_base_done_linux_x86_64"); // a non-digit byte terminates the scan + emitter.instruction("cmp rax, 122"); // is the byte above 'z'? + emitter.instruction("ja __rt_str_to_int_base_done_linux_x86_64"); // a non-digit byte terminates the scan + emitter.instruction("mov rdx, rax"); // copy the byte before deriving its letter value + emitter.instruction("sub rdx, 87"); // map 'a'..'z' to digit values 10..35 + + emitter.label("__rt_str_to_int_base_digit_linux_x86_64"); + emitter.instruction("cmp rdx, rcx"); // is the decoded digit valid in the resolved base? + emitter.instruction("jae __rt_str_to_int_base_done_linux_x86_64"); // a digit outside the base terminates the scan + emitter.instruction("cmp r9, r11"); // would shifting the accumulator already pass the limit? + emitter.instruction("ja __rt_str_to_int_base_saturate_linux_x86_64"); // an accumulator past the threshold can only overflow + emitter.instruction("mov rax, r9"); // stage the accumulator for the digit shift + emitter.instruction("imul rax, rcx"); // shift the accumulator up by one digit position + emitter.instruction("mov r9, r10"); // copy the limit before deriving the per-digit headroom + emitter.instruction("sub r9, rdx"); // compute the largest accumulator this digit still fits in + emitter.instruction("cmp rax, r9"); // would adding this digit pass the limit? + emitter.instruction("ja __rt_str_to_int_base_saturate_linux_x86_64"); // clamp instead of wrapping past PHP_INT_MAX/PHP_INT_MIN + emitter.instruction("add rax, rdx"); // accumulate the digit into the unsigned result + emitter.instruction("mov r9, rax"); // keep the updated accumulator + emitter.instruction("jmp __rt_str_to_int_base_loop_linux_x86_64"); // continue scanning the remaining input bytes + + emitter.label("__rt_str_to_int_base_saturate_linux_x86_64"); + emitter.instruction("mov r9, r10"); // clamp to the limit this sign allows + + emitter.label("__rt_str_to_int_base_done_linux_x86_64"); + emitter.instruction("mov rax, r9"); // return the accumulated magnitude + emitter.instruction("test r8, r8"); // did the scan consume a minus sign? + emitter.instruction("jz __rt_str_to_int_base_ret_linux_x86_64"); // a positive parse is already the result + emitter.instruction("neg rax"); // apply the consumed minus sign + emitter.label("__rt_str_to_int_base_ret_linux_x86_64"); + emitter.instruction("ret"); // hand the parsed integer back to the caller + + emitter.label("__rt_str_to_int_base_zero_linux_x86_64"); + emitter.instruction("xor eax, eax"); // an unusable base or digit-free string parses to zero + emitter.instruction("ret"); // hand the zero result back to the caller +} diff --git a/src/codegen_support/runtime/strings/str_to_number.rs b/src/codegen_support/runtime/strings/str_to_number.rs index eda1dbecc3..9ebad984f8 100644 --- a/src/codegen_support/runtime/strings/str_to_number.rs +++ b/src/codegen_support/runtime/strings/str_to_number.rs @@ -1,22 +1,32 @@ //! Purpose: -//! Emits string numeric-detection helpers used by PHP loose comparison and int-parameter coercion. -//! Converts pointer/length PHP strings through libc `strtod` while rejecting trailing junk. +//! Emits string numeric-detection helpers used by PHP loose comparison, `is_numeric()`, +//! the `(float)` string cast, and int-parameter coercion. +//! Converts pointer/length PHP strings through libc `strtod` after clipping them to +//! PHP's own numeric-string grammar. //! //! Called from: //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. //! //! Key details: //! - The helper returns both a numeric flag and the parsed double without losing PHP byte-string bounds. -//! - The enum/int coercion probe rejects libc-only spellings such as hex floats, INF, and NAN. +//! - `__rt_php_num_scan` runs between `__rt_cstr` and `strtod`, so libc never sees the +//! spellings PHP's grammar rejects: hexadecimal (`"0x1A"` is `0`, not `26`), `INF` / +//! `INFINITY` / `NAN` (all `0.0`), and underscore separators. It also owns the +//! leading/trailing PHP-whitespace rules, so `" 42 "` is numeric while `"1e"` is not. +//! - The numeric flag is PHP's `is_numeric_string(..., allow_errors = 0)`; the parsed +//! double is always the value of the *leading* numeric run, which is what the +//! `(float)` cast wants even for `"12abc"`. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; /// Emits `__rt_str_to_number`: converts a PHP string to a double and reports whether it is numeric. -/// Copies the PHP string into the C-string scratch buffer via `__rt_cstr`, then parses it with libc `strtod`. -/// The parsed double is returned in d0/xmm0; the integer result register is 1 when the string is fully -/// numeric (strtod consumed at least one byte and trailing bytes are all ASCII space or tab/newline/form-feed/carriage-return), -/// 0 otherwise. +/// +/// Copies the PHP string into the C-string scratch buffer via `__rt_cstr`, clips it to PHP's +/// leading numeric run with `__rt_php_num_scan`, then parses that run with libc `strtod`. +/// The parsed double is returned in d0/xmm0 (`0.0` when the string has no numeric prefix); +/// the integer result register is 1 when the whole string was numeric (PHP whitespace on +/// either side is allowed), 0 otherwise. /// Dispatches to the x86_64-specific implementation when targeting that architecture. pub fn emit_str_to_number(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -28,45 +38,25 @@ pub fn emit_str_to_number(emitter: &mut Emitter) { emitter.comment("--- runtime: str_to_number ---"); emitter.label_global("__rt_str_to_number"); - emitter.instruction("sub sp, sp, #32"); // allocate helper slots for the C string start and strtod end pointer + emitter.instruction("sub sp, sp, #32"); // allocate a helper slot for the numeric flag emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address emitter.instruction("add x29, sp, #16"); // establish a stable helper frame pointer emitter.instruction("bl __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("str x0, [sp, #0]"); // save the C-string start pointer for the no-consumption check - emitter.instruction("add x1, sp, #8"); // pass the address of the local end-pointer slot to strtod - emitter.bl_c("strtod"); - emitter.instruction("ldr x9, [sp, #8]"); // load the end pointer returned by strtod - emitter.instruction("ldr x10, [sp, #0]"); // reload the C-string start pointer - emitter.instruction("cmp x9, x10"); // reject strings where strtod consumed no numeric bytes - emitter.instruction("b.eq __rt_str_to_number_false"); // no consumed bytes means this is not a numeric string - - emitter.label("__rt_str_to_number_trailing_loop"); - emitter.instruction("ldrb w11, [x9], #1"); // load the next trailing byte and advance the scan cursor - emitter.instruction("cbz w11, __rt_str_to_number_true"); // end of C string means all trailing bytes were acceptable - emitter.instruction("cmp w11, #32"); // ASCII space is allowed after the numeric payload - emitter.instruction("b.eq __rt_str_to_number_trailing_loop"); // keep scanning after an allowed space - emitter.instruction("sub w12, w11, #9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp w12, #4"); // values 9 through 13 are accepted trailing whitespace - emitter.instruction("b.ls __rt_str_to_number_trailing_loop"); // keep scanning after accepted control whitespace - - emitter.label("__rt_str_to_number_false"); - emitter.instruction("mov x0, #0"); // report that the string is not numeric - emitter.instruction("b __rt_str_to_number_done"); // restore the helper frame and return + emitter.instruction("bl __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("str x1, [sp, #0]"); // save the fully-numeric flag across strtod + emitter.instruction("mov x1, #0"); // strtod endptr = NULL: the run is already clipped + emitter.bl_c("strtod"); // parse the clipped numeric run into d0 + emitter.instruction("ldr x0, [sp, #0]"); // reload the fully-numeric flag as the result - emitter.label("__rt_str_to_number_true"); - emitter.instruction("mov x0, #1"); // report that the string parsed as a complete numeric string - - emitter.label("__rt_str_to_number_done"); emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #32"); // release the helper stack frame emitter.instruction("ret"); // return the numeric flag while preserving the parsed double in d0 } /// Emits `__rt_str_to_number` for the Linux x86_64 target. Identical logic to the ARM64 path but using -/// x86_64 calling conventions: copies the PHP string via `__rt_cstr`, parses with libc `strtod`, checks -/// that strtod consumed at least one byte, then validates that all trailing bytes are ASCII space or -/// tab/newline/form-feed/carriage-return. Returns 1 in rax when fully numeric, 0 otherwise; parsed -/// double is preserved in xmm0. +/// x86_64 calling conventions: copies the PHP string via `__rt_cstr`, clips it with +/// `__rt_php_num_scan`, then parses the clipped run with libc `strtod`. Returns 1 in rax when the +/// whole string was numeric, 0 otherwise; the parsed double is preserved in xmm0. fn emit_str_to_number_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: str_to_number ---"); @@ -74,35 +64,16 @@ fn emit_str_to_number_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // save the caller frame pointer before nested libc calls emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // allocate aligned helper slots for start and end pointers + emitter.instruction("sub rsp, 32"); // allocate an aligned helper slot for the numeric flag emitter.instruction("call __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the C-string start pointer for the no-consumption check - emitter.instruction("lea rsi, [rbp - 16]"); // pass the address of the local end-pointer slot to strtod - emitter.instruction("mov rdi, rax"); // pass the C-string start pointer as strtod's first argument - emitter.instruction("call strtod"); // parse the C string as a double through libc - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // load the end pointer returned by strtod - emitter.instruction("cmp r8, QWORD PTR [rbp - 8]"); // reject strings where strtod consumed no numeric bytes - emitter.instruction("je __rt_str_to_number_false_linux_x86_64"); // no consumed bytes means this is not a numeric string - - emitter.label("__rt_str_to_number_trailing_loop_linux_x86_64"); - emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the next trailing byte without sign extension - emitter.instruction("add r8, 1"); // advance the trailing-byte scan cursor - emitter.instruction("test r9d, r9d"); // check whether the scan reached the C-string terminator - emitter.instruction("je __rt_str_to_number_true_linux_x86_64"); // end of C string means all trailing bytes were acceptable - emitter.instruction("cmp r9d, 32"); // ASCII space is allowed after the numeric payload - emitter.instruction("je __rt_str_to_number_trailing_loop_linux_x86_64"); // keep scanning after an allowed space - emitter.instruction("sub r9d, 9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp r9d, 4"); // values 9 through 13 are accepted trailing whitespace - emitter.instruction("jbe __rt_str_to_number_trailing_loop_linux_x86_64"); // keep scanning after accepted control whitespace + emitter.instruction("mov rdi, rax"); // pass the C-string pointer to the numeric-grammar scanner + emitter.instruction("call __rt_php_num_scan"); // clip the scratch to PHP's leading numeric run + emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // save the fully-numeric flag across strtod + emitter.instruction("mov rdi, rax"); // pass the clipped numeric run as strtod's first argument + emitter.instruction("xor esi, esi"); // strtod endptr = NULL: the run is already clipped + emitter.instruction("call strtod"); // parse the clipped numeric run into xmm0 + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the fully-numeric flag as the result - emitter.label("__rt_str_to_number_false_linux_x86_64"); - emitter.instruction("xor rax, rax"); // report that the string is not numeric - emitter.instruction("jmp __rt_str_to_number_done_linux_x86_64"); // restore the helper frame and return - - emitter.label("__rt_str_to_number_true_linux_x86_64"); - emitter.instruction("mov rax, 1"); // report that the string parsed as a complete numeric string - - emitter.label("__rt_str_to_number_done_linux_x86_64"); emitter.instruction("add rsp, 32"); // release the helper stack frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the numeric flag while preserving the parsed double in xmm0 @@ -110,10 +81,10 @@ fn emit_str_to_number_linux_x86_64(emitter: &mut Emitter) { /// Emits `__rt_str_looks_like_int_for_coercion` for PHP string-to-int parameter coercion. /// -/// The helper accepts the same bounded string inputs as `__rt_str_to_number`, but rejects libc -/// `strtod` extensions that PHP does not accept for coercive `int` parameters: hexadecimal -/// float prefixes (`0x`/`0X`) and case-insensitive `INF`/`INFINITY`/`NAN` spellings after -/// optional leading whitespace and sign. +/// PHP coerces a string into an `int` parameter only when it is a *numeric string*, so this +/// helper is exactly `__rt_php_num_scan`'s fully-numeric flag: leading and trailing PHP +/// whitespace are allowed, while `"0x1A"`, `"INF"`, `"NAN"`, `"1e"` and `"12abc"` are all +/// rejected because the grammar either does not reach them or leaves trailing garbage. pub fn emit_str_looks_like_int_for_coercion(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_str_looks_like_int_for_coercion_linux_x86_64(emitter); @@ -124,214 +95,36 @@ pub fn emit_str_looks_like_int_for_coercion(emitter: &mut Emitter) { emitter.comment("--- runtime: str_looks_like_int_for_coercion ---"); emitter.label_global("__rt_str_looks_like_int_for_coercion"); - emitter.instruction("sub sp, sp, #32"); // allocate helper slots for the C string start and strtod end pointer - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #16"); // establish a stable helper frame pointer + emitter.instruction("sub sp, sp, #16"); // allocate the minimum aligned helper frame + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish a stable helper frame pointer emitter.instruction("bl __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("str x0, [sp, #0]"); // save the C-string start pointer for validation and parsing - emitter.instruction("mov x9, x0"); // scan from the C-string start for PHP-forbidden prefixes - - emitter.label("__rt_sliic_ws"); - emitter.instruction("ldrb w10, [x9]"); // load the next byte while skipping leading whitespace - emitter.instruction("cmp w10, #32"); // ASCII space is allowed before the numeric payload - emitter.instruction("b.eq __rt_sliic_ws_next"); // skip an allowed leading space - emitter.instruction("sub w11, w10, #9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp w11, #4"); // values 9 through 13 are accepted leading whitespace - emitter.instruction("b.ls __rt_sliic_ws_next"); // skip accepted control whitespace - emitter.instruction("b __rt_sliic_sign"); // inspect an optional sign before checking prefixes - - emitter.label("__rt_sliic_ws_next"); - emitter.instruction("add x9, x9, #1"); // advance past one leading whitespace byte - emitter.instruction("b __rt_sliic_ws"); // keep scanning leading whitespace - - emitter.label("__rt_sliic_sign"); - emitter.instruction("cmp w10, #43"); // plus sign may precede the numeric payload - emitter.instruction("b.eq __rt_sliic_after_sign"); // skip a leading plus sign - emitter.instruction("cmp w10, #45"); // minus sign may precede the numeric payload - emitter.instruction("b.ne __rt_sliic_special"); // no sign: validate the current payload byte - - emitter.label("__rt_sliic_after_sign"); - emitter.instruction("add x9, x9, #1"); // advance past the optional sign byte - emitter.instruction("ldrb w10, [x9]"); // reload the first payload byte after the sign - - emitter.label("__rt_sliic_special"); - emitter.instruction("cmp w10, #48"); // hexadecimal floats begin with 0x or 0X after sign/whitespace - emitter.instruction("b.ne __rt_sliic_check_inf"); // non-zero prefixes cannot be libc hexadecimal floats - emitter.instruction("ldrb w11, [x9, #1]"); // inspect the byte after the leading zero - emitter.instruction("cmp w11, #120"); // lowercase x marks a libc hexadecimal float - emitter.instruction("b.eq __rt_sliic_false"); // reject 0x-prefixed strings for int parameter coercion - emitter.instruction("cmp w11, #88"); // uppercase X marks a libc hexadecimal float - emitter.instruction("b.eq __rt_sliic_false"); // reject 0X-prefixed strings for int parameter coercion - emitter.instruction("b __rt_sliic_parse"); // ordinary zero-prefixed decimal strings remain valid candidates - - emitter.label("__rt_sliic_check_inf"); - emitter.instruction("orr w11, w10, #0x20"); // fold the first payload byte to lowercase ASCII - emitter.instruction("cmp w11, #105"); // lowercase i starts libc INF/INFINITY spellings - emitter.instruction("b.eq __rt_sliic_inf"); // verify and reject an INF prefix - emitter.instruction("cmp w11, #110"); // lowercase n starts libc NAN spellings - emitter.instruction("b.eq __rt_sliic_nan"); // verify and reject a NAN prefix - emitter.instruction("b __rt_sliic_parse"); // other prefixes are handled by the numeric parser - - emitter.label("__rt_sliic_inf"); - emitter.instruction("ldrb w12, [x9, #1]"); // load the second INF byte - emitter.instruction("orr w12, w12, #0x20"); // fold the second INF byte to lowercase ASCII - emitter.instruction("cmp w12, #110"); // require n after i for INF - emitter.instruction("b.ne __rt_sliic_parse"); // not INF: let strtod/trailing checks decide - emitter.instruction("ldrb w12, [x9, #2]"); // load the third INF byte - emitter.instruction("orr w12, w12, #0x20"); // fold the third INF byte to lowercase ASCII - emitter.instruction("cmp w12, #102"); // require f after in for INF - emitter.instruction("b.eq __rt_sliic_false"); // reject INF and INFINITY spellings - emitter.instruction("b __rt_sliic_parse"); // not INF: let strtod/trailing checks decide - - emitter.label("__rt_sliic_nan"); - emitter.instruction("ldrb w12, [x9, #1]"); // load the second NAN byte - emitter.instruction("orr w12, w12, #0x20"); // fold the second NAN byte to lowercase ASCII - emitter.instruction("cmp w12, #97"); // require a after n for NAN - emitter.instruction("b.ne __rt_sliic_parse"); // not NAN: let strtod/trailing checks decide - emitter.instruction("ldrb w12, [x9, #2]"); // load the third NAN byte - emitter.instruction("orr w12, w12, #0x20"); // fold the third NAN byte to lowercase ASCII - emitter.instruction("cmp w12, #110"); // require n after na for NAN - emitter.instruction("b.eq __rt_sliic_false"); // reject NAN spellings - - emitter.label("__rt_sliic_parse"); - emitter.instruction("ldr x0, [sp, #0]"); // reload the C-string start pointer for strtod - emitter.instruction("add x1, sp, #8"); // pass the address of the local end-pointer slot to strtod - emitter.bl_c("strtod"); - emitter.instruction("ldr x9, [sp, #8]"); // load the end pointer returned by strtod - emitter.instruction("ldr x10, [sp, #0]"); // reload the C-string start pointer - emitter.instruction("cmp x9, x10"); // reject strings where strtod consumed no numeric bytes - emitter.instruction("b.eq __rt_sliic_false"); // no consumed bytes means this is not coercible to int - - emitter.label("__rt_sliic_trailing"); - emitter.instruction("ldrb w11, [x9], #1"); // load the next trailing byte and advance the scan cursor - emitter.instruction("cbz w11, __rt_sliic_true"); // end of C string means all trailing bytes were acceptable - emitter.instruction("cmp w11, #32"); // ASCII space is allowed after the numeric payload - emitter.instruction("b.eq __rt_sliic_trailing"); // keep scanning after an allowed space - emitter.instruction("sub w12, w11, #9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp w12, #4"); // values 9 through 13 are accepted trailing whitespace - emitter.instruction("b.ls __rt_sliic_trailing"); // keep scanning after accepted control whitespace + emitter.instruction("bl __rt_php_num_scan"); // classify the scratch under PHP's numeric-string grammar + emitter.instruction("mov x0, x1"); // the coercion flag is the fully-numeric flag - emitter.label("__rt_sliic_false"); - emitter.instruction("mov x0, #0"); // report that the string cannot coerce to an int parameter - emitter.instruction("b __rt_sliic_done"); // restore the helper frame and return - - emitter.label("__rt_sliic_true"); - emitter.instruction("mov x0, #1"); // report that the string can coerce to an int parameter - - emitter.label("__rt_sliic_done"); - emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #32"); // release the helper stack frame + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the helper stack frame emitter.instruction("ret"); // return the coercion flag } /// Emits the Linux x86_64 variant of `__rt_str_looks_like_int_for_coercion`. /// -/// The x86_64 implementation mirrors the AArch64 helper while using SysV argument registers -/// for `strtod`; it returns 1 in `rax` for strings PHP may coerce into an `int` parameter. +/// Same contract as the AArch64 helper: returns 1 in `rax` for strings PHP may coerce into +/// an `int` parameter, driven entirely by `__rt_php_num_scan`'s fully-numeric flag. fn emit_str_looks_like_int_for_coercion_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: str_looks_like_int_for_coercion ---"); emitter.label_global("__rt_str_looks_like_int_for_coercion"); - emitter.instruction("push rbp"); // save the caller frame pointer before nested libc calls + emitter.instruction("push rbp"); // save the caller frame pointer before nested runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // allocate aligned helper slots for start and end pointers + emitter.instruction("sub rsp, 16"); // keep the stack aligned for the nested calls emitter.instruction("call __rt_cstr"); // copy the bounded PHP string into the C-string scratch buffer - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the C-string start pointer for validation and parsing - emitter.instruction("mov r8, rax"); // scan from the C-string start for PHP-forbidden prefixes - - emitter.label("__rt_sliic_ws_x"); - emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the next byte while skipping leading whitespace - emitter.instruction("cmp r9d, 32"); // ASCII space is allowed before the numeric payload - emitter.instruction("je __rt_sliic_ws_next_x"); // skip an allowed leading space - emitter.instruction("mov r10d, r9d"); // copy the byte before normalizing control whitespace - emitter.instruction("sub r10d, 9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp r10d, 4"); // values 9 through 13 are accepted leading whitespace - emitter.instruction("jbe __rt_sliic_ws_next_x"); // skip accepted control whitespace - emitter.instruction("jmp __rt_sliic_sign_x"); // inspect an optional sign before checking prefixes - - emitter.label("__rt_sliic_ws_next_x"); - emitter.instruction("add r8, 1"); // advance past one leading whitespace byte - emitter.instruction("jmp __rt_sliic_ws_x"); // keep scanning leading whitespace - - emitter.label("__rt_sliic_sign_x"); - emitter.instruction("cmp r9d, 43"); // plus sign may precede the numeric payload - emitter.instruction("je __rt_sliic_after_sign_x"); // skip a leading plus sign - emitter.instruction("cmp r9d, 45"); // minus sign may precede the numeric payload - emitter.instruction("jne __rt_sliic_special_x"); // no sign: validate the current payload byte - - emitter.label("__rt_sliic_after_sign_x"); - emitter.instruction("add r8, 1"); // advance past the optional sign byte - emitter.instruction("movzx r9d, BYTE PTR [r8]"); // reload the first payload byte after the sign + emitter.instruction("mov rdi, rax"); // pass the C-string pointer to the numeric-grammar scanner + emitter.instruction("call __rt_php_num_scan"); // classify the scratch under PHP's numeric-string grammar + emitter.instruction("mov rax, rdx"); // the coercion flag is the fully-numeric flag - emitter.label("__rt_sliic_special_x"); - emitter.instruction("cmp r9d, 48"); // hexadecimal floats begin with 0x or 0X after sign/whitespace - emitter.instruction("jne __rt_sliic_check_inf_x"); // non-zero prefixes cannot be libc hexadecimal floats - emitter.instruction("movzx r10d, BYTE PTR [r8 + 1]"); // inspect the byte after the leading zero - emitter.instruction("cmp r10d, 120"); // lowercase x marks a libc hexadecimal float - emitter.instruction("je __rt_sliic_false_x"); // reject 0x-prefixed strings for int parameter coercion - emitter.instruction("cmp r10d, 88"); // uppercase X marks a libc hexadecimal float - emitter.instruction("je __rt_sliic_false_x"); // reject 0X-prefixed strings for int parameter coercion - emitter.instruction("jmp __rt_sliic_parse_x"); // ordinary zero-prefixed decimal strings remain valid candidates - - emitter.label("__rt_sliic_check_inf_x"); - emitter.instruction("mov r10d, r9d"); // copy the first payload byte before case folding - emitter.instruction("or r10d, 32"); // fold the first payload byte to lowercase ASCII - emitter.instruction("cmp r10d, 105"); // lowercase i starts libc INF/INFINITY spellings - emitter.instruction("je __rt_sliic_inf_x"); // verify and reject an INF prefix - emitter.instruction("cmp r10d, 110"); // lowercase n starts libc NAN spellings - emitter.instruction("je __rt_sliic_nan_x"); // verify and reject a NAN prefix - emitter.instruction("jmp __rt_sliic_parse_x"); // other prefixes are handled by the numeric parser - - emitter.label("__rt_sliic_inf_x"); - emitter.instruction("movzx r11d, BYTE PTR [r8 + 1]"); // load the second INF byte - emitter.instruction("or r11d, 32"); // fold the second INF byte to lowercase ASCII - emitter.instruction("cmp r11d, 110"); // require n after i for INF - emitter.instruction("jne __rt_sliic_parse_x"); // not INF: let strtod/trailing checks decide - emitter.instruction("movzx r11d, BYTE PTR [r8 + 2]"); // load the third INF byte - emitter.instruction("or r11d, 32"); // fold the third INF byte to lowercase ASCII - emitter.instruction("cmp r11d, 102"); // require f after in for INF - emitter.instruction("je __rt_sliic_false_x"); // reject INF and INFINITY spellings - emitter.instruction("jmp __rt_sliic_parse_x"); // not INF: let strtod/trailing checks decide - - emitter.label("__rt_sliic_nan_x"); - emitter.instruction("movzx r11d, BYTE PTR [r8 + 1]"); // load the second NAN byte - emitter.instruction("or r11d, 32"); // fold the second NAN byte to lowercase ASCII - emitter.instruction("cmp r11d, 97"); // require a after n for NAN - emitter.instruction("jne __rt_sliic_parse_x"); // not NAN: let strtod/trailing checks decide - emitter.instruction("movzx r11d, BYTE PTR [r8 + 2]"); // load the third NAN byte - emitter.instruction("or r11d, 32"); // fold the third NAN byte to lowercase ASCII - emitter.instruction("cmp r11d, 110"); // require n after na for NAN - emitter.instruction("je __rt_sliic_false_x"); // reject NAN spellings - - emitter.label("__rt_sliic_parse_x"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the C-string start pointer for strtod - emitter.instruction("lea rsi, [rbp - 16]"); // pass the address of the local end-pointer slot to strtod - emitter.instruction("call strtod"); // parse the C string as a double through libc - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // load the end pointer returned by strtod - emitter.instruction("cmp r8, QWORD PTR [rbp - 8]"); // reject strings where strtod consumed no numeric bytes - emitter.instruction("je __rt_sliic_false_x"); // no consumed bytes means this is not coercible to int - - emitter.label("__rt_sliic_trailing_x"); - emitter.instruction("movzx r9d, BYTE PTR [r8]"); // load the next trailing byte without sign extension - emitter.instruction("add r8, 1"); // advance the trailing-byte scan cursor - emitter.instruction("test r9d, r9d"); // check whether the scan reached the C-string terminator - emitter.instruction("je __rt_sliic_true_x"); // end of C string means all trailing bytes were acceptable - emitter.instruction("cmp r9d, 32"); // ASCII space is allowed after the numeric payload - emitter.instruction("je __rt_sliic_trailing_x"); // keep scanning after an allowed space - emitter.instruction("sub r9d, 9"); // normalize ASCII tab/newline/form-feed/carriage-return range - emitter.instruction("cmp r9d, 4"); // values 9 through 13 are accepted trailing whitespace - emitter.instruction("jbe __rt_sliic_trailing_x"); // keep scanning after accepted control whitespace - - emitter.label("__rt_sliic_false_x"); - emitter.instruction("xor rax, rax"); // report that the string cannot coerce to an int parameter - emitter.instruction("jmp __rt_sliic_done_x"); // restore the helper frame and return - - emitter.label("__rt_sliic_true_x"); - emitter.instruction("mov rax, 1"); // report that the string can coerce to an int parameter - - emitter.label("__rt_sliic_done_x"); - emitter.instruction("add rsp, 32"); // release the helper stack frame + emitter.instruction("add rsp, 16"); // release the helper stack frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the coercion flag } diff --git a/src/codegen_support/runtime/strings/str_word_count.rs b/src/codegen_support/runtime/strings/str_word_count.rs new file mode 100644 index 0000000000..a46d293368 --- /dev/null +++ b/src/codegen_support/runtime/strings/str_word_count.rs @@ -0,0 +1,397 @@ +//! Purpose: +//! Emits the `__rt_str_word_count` runtime helper assembly for PHP's `str_word_count`: +//! scans the subject for php-src's word definition and materializes the requested result +//! shape (a count, a list of words, or a byte-offset map). +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - php-src's word alphabet is `isalpha()` under the C locale plus `'` and `-`, extended by +//! every byte in the optional `$characters` list. A 256-byte membership table is built on +//! the helper's own frame, so the scan never re-walks the character list. +//! - php-src trims a LEADING `'` or `-` and a TRAILING `-` before scanning, unless the +//! character list explicitly re-admits that byte. Both adjustments are reproduced verbatim. +//! - Format 1 pushes each word through `__rt_array_push_str`, which persists the bytes, and +//! format 2 persists the word itself before `__rt_hash_set` stores it under its byte offset. +//! Neither result can alias the subject, which is what the `Fresh` ownership contract on +//! `RuntimeFnId::StrWordCount` promises. +//! - An unknown `$format` never reaches this helper: the EIR lowering raises php-src's +//! catchable `ValueError` before the call. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Byte offset of the 256-entry word-character membership table inside the helper frame. +const MASK_BYTES: usize = 256; + +/// Emits the `__rt_str_word_count` runtime helper. +/// +/// ABI (AArch64): +/// Input: `x1` = subject pointer, `x2` = subject length, `x3` = format (0, 1, or 2), +/// `x4` = character-list pointer, `x5` = character-list length. +/// Output: `x0` = word count (format 0), indexed array pointer (format 1), or hash +/// pointer (format 2). +/// +/// ABI (x86_64 System V): +/// Input: `rax` = subject pointer, `rdx` = subject length, `rdi` = format, +/// `rcx` = character-list pointer, `r8` = character-list length. +/// Output: `rax` = the same three result shapes. +/// +/// Clobbers every caller-saved register: the container paths reach `__rt_array_new`, +/// `__rt_hash_new`, `__rt_array_push_str`, `__rt_str_persist`, and `__rt_hash_set`. +pub fn emit_str_word_count(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_str_word_count_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: str_word_count ---"); + emitter.label_global("__rt_str_word_count"); + + // Frame layout (352 bytes): + // [sp, #0] = 256-byte word-character membership table + // [sp, #256] = subject base pointer + // [sp, #264] = subject length + // [sp, #272] = requested format + // [sp, #280] = scan cursor `p` + // [sp, #288] = scan end `e` + // [sp, #296] = running result (count, array pointer, or hash pointer) + // [sp, #304] = current word start + // [sp, #312] = current word length + // [sp, #320] = current word byte offset (format 2 hash key) + // [sp, #336] = saved x29/x30 + emitter.instruction("sub sp, sp, #352"); // allocate the word-scan frame with its membership table + emitter.instruction("stp x29, x30, [sp, #336]"); // save the frame pointer and return address across the container helper calls + emitter.instruction("add x29, sp, #336"); // establish the str_word_count helper frame pointer + emitter.instruction("str x1, [sp, #256]"); // save the subject base pointer for the format 2 offset keys + emitter.instruction("str x2, [sp, #264]"); // save the subject length across the container allocation + emitter.instruction("str x3, [sp, #272]"); // save the requested result format + + // -- clear the 256-entry word-character membership table -- + emitter.instruction("mov x10, sp"); // x10 = membership table base, recomputed after every helper call + emitter.instruction("mov x9, #0"); // start clearing at the first table word + emitter.label("__rt_str_word_count_zero"); + emitter.instruction("str xzr, [x10, x9]"); // clear eight membership entries at a time + emitter.instruction("add x9, x9, #8"); // advance to the next table word + emitter.instruction(&format!("cmp x9, #{}", MASK_BYTES)); // has the whole table been cleared? + emitter.instruction("b.lo __rt_str_word_count_zero"); // keep clearing until the table is fully zeroed + + // -- admit every byte of the optional `$characters` list as a word character -- + emitter.instruction("mov x9, #0"); // start at the first character-list byte + emitter.label("__rt_str_word_count_mask"); + emitter.instruction("cmp x9, x5"); // has the whole character list been consumed? + emitter.instruction("b.hs __rt_str_word_count_mask_done"); // the membership table is complete + emitter.instruction("ldrb w11, [x4, x9]"); // load the next extra word character + emitter.instruction("mov w12, #1"); // membership marker for an admitted byte + emitter.instruction("strb w12, [x10, x11]"); // admit the byte into the word alphabet + emitter.instruction("add x9, x9, #1"); // advance to the next character-list byte + emitter.instruction("b __rt_str_word_count_mask"); // keep admitting character-list bytes + emitter.label("__rt_str_word_count_mask_done"); + + // -- allocate the container the requested format needs -- + emitter.instruction("mov x0, xzr"); // format 0 accumulates a plain word count starting at zero + emitter.instruction("ldr x3, [sp, #272]"); // reload the requested result format + emitter.instruction("cbz x3, __rt_str_word_count_container_ready"); // format 0 needs no container at all + emitter.instruction("cmp x3, #1"); // is the caller asking for the list of words? + emitter.instruction("b.ne __rt_str_word_count_map_new"); // format 2 builds the byte-offset map instead + emitter.instruction("mov x0, #16"); // seed the word list with room for sixteen entries + emitter.instruction("mov x1, #16"); // 16-byte slots hold the (pointer, length) string pairs + emitter.instruction("bl __rt_array_new"); // allocate the indexed result array + emitter.instruction("b __rt_str_word_count_container_ready"); // the list container is ready + emitter.label("__rt_str_word_count_map_new"); + emitter.instruction("mov x0, #8"); // seed the byte-offset map with the minimum hash capacity + emitter.instruction("mov x1, #1"); // value_type 1 = string values + emitter.instruction("bl __rt_hash_new"); // allocate the byte-offset result hash + emitter.label("__rt_str_word_count_container_ready"); + emitter.instruction("str x0, [sp, #296]"); // publish the running result before the scan starts + emitter.instruction("mov x10, sp"); // restore the membership table base after the container allocation + + // -- an empty subject yields the count 0 or the empty container -- + emitter.instruction("ldr x1, [sp, #256]"); // reload the subject base pointer + emitter.instruction("ldr x2, [sp, #264]"); // reload the subject length + emitter.instruction("cbz x2, __rt_str_word_count_done"); // php-src returns immediately for an empty subject + emitter.instruction("add x3, x1, x2"); // scan end = subject base + subject length + + // -- php-src drops a leading `'` or `-` unless the character list re-admits it -- + emitter.instruction("ldrb w11, [x1]"); // load the subject's first byte + emitter.instruction("cmp w11, #39"); // is the first byte an apostrophe? + emitter.instruction("b.ne __rt_str_word_count_first_dash"); // check the leading hyphen case instead + emitter.instruction("ldrb w13, [x10, #39]"); // is the apostrophe an explicitly admitted word character? + emitter.instruction("cbnz w13, __rt_str_word_count_first_done"); // an admitted apostrophe may open a word + emitter.instruction("add x1, x1, #1"); // skip the leading apostrophe + emitter.instruction("b __rt_str_word_count_first_done"); // php-src performs at most one leading skip + emitter.label("__rt_str_word_count_first_dash"); + emitter.instruction("cmp w11, #45"); // is the first byte a hyphen? + emitter.instruction("b.ne __rt_str_word_count_first_done"); // any other first byte is scanned normally + emitter.instruction("ldrb w13, [x10, #45]"); // is the hyphen an explicitly admitted word character? + emitter.instruction("cbnz w13, __rt_str_word_count_first_done"); // an admitted hyphen may open a word + emitter.instruction("add x1, x1, #1"); // skip the leading hyphen + emitter.label("__rt_str_word_count_first_done"); + + // -- php-src drops a trailing `-` unless the character list re-admits it -- + emitter.instruction("sub x9, x3, #1"); // address the subject's last byte + emitter.instruction("ldrb w11, [x9]"); // load the subject's last byte + emitter.instruction("cmp w11, #45"); // is the last byte a hyphen? + emitter.instruction("b.ne __rt_str_word_count_last_done"); // any other last byte stays inside the scan range + emitter.instruction("ldrb w13, [x10, #45]"); // is the hyphen an explicitly admitted word character? + emitter.instruction("cbnz w13, __rt_str_word_count_last_done"); // an admitted hyphen may close a word + emitter.instruction("sub x3, x3, #1"); // exclude the trailing hyphen from the scan range + emitter.label("__rt_str_word_count_last_done"); + emitter.instruction("str x1, [sp, #280]"); // publish the adjusted scan cursor + emitter.instruction("str x3, [sp, #288]"); // publish the adjusted scan end + + // -- walk the subject one candidate word at a time -- + emitter.label("__rt_str_word_count_scan"); + emitter.instruction("ldr x1, [sp, #280]"); // reload the scan cursor + emitter.instruction("ldr x3, [sp, #288]"); // reload the scan end + emitter.instruction("cmp x1, x3"); // has the scan reached the adjusted end? + emitter.instruction("b.hs __rt_str_word_count_done"); // every candidate word has been visited + emitter.instruction("mov x4, x1"); // remember where this candidate word starts + emitter.instruction("str x4, [sp, #304]"); // save the word start across the recording helper calls + + emitter.label("__rt_str_word_count_word"); + emitter.instruction("cmp x1, x3"); // is the cursor still inside the scan range? + emitter.instruction("b.hs __rt_str_word_count_word_end"); // the word ends at the scan end + emitter.instruction("ldrb w11, [x1]"); // load the candidate word byte + emitter.instruction("orr w12, w11, #0x20"); // fold the byte to lower case for the C-locale isalpha() test + emitter.instruction("sub w12, w12, #97"); // rebase the folded byte on 'a' + emitter.instruction("cmp w12, #26"); // is the byte an ASCII letter? + emitter.instruction("b.lo __rt_str_word_count_word_char"); // letters always continue the word + emitter.instruction("ldrb w13, [x10, x11]"); // is the byte an explicitly admitted word character? + emitter.instruction("cbnz w13, __rt_str_word_count_word_char"); // admitted bytes continue the word + emitter.instruction("cmp w11, #39"); // is the byte an interior apostrophe? + emitter.instruction("b.eq __rt_str_word_count_word_char"); // apostrophes always continue the word + emitter.instruction("cmp w11, #45"); // is the byte an interior hyphen? + emitter.instruction("b.eq __rt_str_word_count_word_char"); // hyphens always continue the word + emitter.instruction("b __rt_str_word_count_word_end"); // any other byte terminates the word + emitter.label("__rt_str_word_count_word_char"); + emitter.instruction("add x1, x1, #1"); // consume the word character + emitter.instruction("b __rt_str_word_count_word"); // test the next byte + + emitter.label("__rt_str_word_count_word_end"); + emitter.instruction("str x1, [sp, #280]"); // publish the cursor before any recording helper call + emitter.instruction("subs x5, x1, x4"); // how many bytes did this candidate word cover? + emitter.instruction("b.eq __rt_str_word_count_advance"); // a zero-length candidate is a separator, not a word + emitter.instruction("str x5, [sp, #312]"); // save the word length across the recording helper calls + emitter.instruction("ldr x3, [sp, #272]"); // reload the requested result format + emitter.instruction("cbnz x3, __rt_str_word_count_record"); // formats 1 and 2 materialize the word itself + emitter.instruction("ldr x9, [sp, #296]"); // reload the running word count + emitter.instruction("add x9, x9, #1"); // count one more word + emitter.instruction("str x9, [sp, #296]"); // publish the updated word count + emitter.instruction("b __rt_str_word_count_advance"); // move past the terminating byte + + emitter.label("__rt_str_word_count_record"); + emitter.instruction("cmp x3, #1"); // is the caller collecting the plain list of words? + emitter.instruction("b.ne __rt_str_word_count_record_map"); // format 2 stores the word under its byte offset + emitter.instruction("ldr x0, [sp, #296]"); // reload the result array pointer + emitter.instruction("ldr x1, [sp, #304]"); // pass the word start to the append helper + emitter.instruction("ldr x2, [sp, #312]"); // pass the word length to the append helper + emitter.instruction("bl __rt_array_push_str"); // append a persisted copy of the word + emitter.instruction("str x0, [sp, #296]"); // republish the array pointer after possible growth + emitter.instruction("b __rt_str_word_count_advance"); // move past the terminating byte + + emitter.label("__rt_str_word_count_record_map"); + emitter.instruction("ldr x9, [sp, #304]"); // reload the word start + emitter.instruction("ldr x11, [sp, #256]"); // reload the subject base pointer + emitter.instruction("sub x9, x9, x11"); // the map key is the word's byte offset in the subject + emitter.instruction("str x9, [sp, #320]"); // save the map key across the persist call + emitter.instruction("ldr x1, [sp, #304]"); // pass the word start to the persist helper + emitter.instruction("ldr x2, [sp, #312]"); // pass the word length to the persist helper + emitter.instruction("bl __rt_str_persist"); // copy the word so the map owns its own bytes + emitter.instruction("mov x3, x1"); // value_lo = owned word pointer + emitter.instruction("mov x4, x2"); // value_hi = owned word length + emitter.instruction("ldr x0, [sp, #296]"); // reload the result hash pointer + emitter.instruction("ldr x1, [sp, #320]"); // key_lo = the word's byte offset + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer key + emitter.instruction("mov x5, #1"); // runtime tag 1 marks the stored value as a string + emitter.instruction("bl __rt_hash_set"); // insert the word under its byte offset + emitter.instruction("str x0, [sp, #296]"); // republish the hash pointer after possible growth + + emitter.label("__rt_str_word_count_advance"); + emitter.instruction("mov x10, sp"); // restore the membership table base after the recording helper calls + emitter.instruction("ldr x1, [sp, #280]"); // reload the scan cursor + emitter.instruction("add x1, x1, #1"); // php-src always steps past the terminating byte + emitter.instruction("str x1, [sp, #280]"); // publish the advanced scan cursor + emitter.instruction("b __rt_str_word_count_scan"); // look for the next candidate word + + emitter.label("__rt_str_word_count_done"); + emitter.instruction("ldr x0, [sp, #296]"); // return the count or the finished container + emitter.instruction("ldp x29, x30, [sp, #336]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #352"); // release the word-scan frame + emitter.instruction("ret"); // return the requested str_word_count result +} + +/// Emits `__rt_str_word_count` for x86_64 Linux using the System V ABI. +/// +/// The membership table sits at the bottom of the frame and is addressed as +/// `[rbp + index - 384]`, so no register has to survive the container helper calls to +/// keep it reachable. The saved-value slots start at `[rbp-32]` to stay clear of the +/// `[rbp-8]`..`[rbp-24]` window other runtime emitters reserve for pushed callee-saved +/// registers. +fn emit_str_word_count_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: str_word_count ---"); + emitter.label_global("__rt_str_word_count"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the container helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the scan state and membership table + emitter.instruction("sub rsp, 384"); // reserve the scan slots plus the 256-byte membership table + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the subject base pointer for the format 2 offset keys + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the subject length across the container allocation + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the requested result format + + // -- clear the 256-entry word-character membership table -- + emitter.instruction("xor r9d, r9d"); // start clearing at the first table word + emitter.label("__rt_str_word_count_zero_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp + r9 - 384], 0"); // clear eight membership entries at a time + emitter.instruction("add r9, 8"); // advance to the next table word + emitter.instruction(&format!("cmp r9, {}", MASK_BYTES)); // has the whole table been cleared? + emitter.instruction("jb __rt_str_word_count_zero_linux_x86_64"); // keep clearing until the table is fully zeroed + + // -- admit every byte of the optional `$characters` list as a word character -- + emitter.instruction("xor r9d, r9d"); // start at the first character-list byte + emitter.label("__rt_str_word_count_mask_linux_x86_64"); + emitter.instruction("cmp r9, r8"); // has the whole character list been consumed? + emitter.instruction("jae __rt_str_word_count_mask_done_linux_x86_64"); // the membership table is complete + emitter.instruction("movzx r10d, BYTE PTR [rcx + r9]"); // load the next extra word character + emitter.instruction("mov BYTE PTR [rbp + r10 - 384], 1"); // admit the byte into the word alphabet + emitter.instruction("add r9, 1"); // advance to the next character-list byte + emitter.instruction("jmp __rt_str_word_count_mask_linux_x86_64"); // keep admitting character-list bytes + emitter.label("__rt_str_word_count_mask_done_linux_x86_64"); + + // -- allocate the container the requested format needs -- + emitter.instruction("xor eax, eax"); // format 0 accumulates a plain word count starting at zero + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload the requested result format + emitter.instruction("test r9, r9"); // is the caller only asking for the word count? + emitter.instruction("jz __rt_str_word_count_ready_linux_x86_64"); // format 0 needs no container at all + emitter.instruction("cmp r9, 1"); // is the caller asking for the list of words? + emitter.instruction("jne __rt_str_word_count_map_new_linux_x86_64"); // format 2 builds the byte-offset map instead + emitter.instruction("mov edi, 16"); // seed the word list with room for sixteen entries + emitter.instruction("mov esi, 16"); // 16-byte slots hold the (pointer, length) string pairs + emitter.instruction("call __rt_array_new"); // allocate the indexed result array + emitter.instruction("jmp __rt_str_word_count_ready_linux_x86_64"); // the list container is ready + emitter.label("__rt_str_word_count_map_new_linux_x86_64"); + emitter.instruction("cmp r9, 2"); // is the caller asking for the byte-offset map? + emitter.instruction("jne __rt_str_word_count_ready_linux_x86_64"); // an unknown format never reaches this helper + emitter.instruction("mov edi, 8"); // seed the byte-offset map with the minimum hash capacity + emitter.instruction("mov esi, 1"); // value_type 1 = string values + emitter.instruction("call __rt_hash_new"); // allocate the byte-offset result hash + emitter.label("__rt_str_word_count_ready_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // publish the running result before the scan starts + + // -- an empty subject yields the count 0 or the empty container -- + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload the subject base pointer as the scan cursor + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // reload the subject length + emitter.instruction("test rdx, rdx"); // is the subject empty? + emitter.instruction("jz __rt_str_word_count_done_linux_x86_64"); // php-src returns immediately for an empty subject + emitter.instruction("mov rdi, rsi"); // copy the subject base before deriving the scan end + emitter.instruction("add rdi, rdx"); // scan end = subject base + subject length + + // -- php-src drops a leading `'` or `-` unless the character list re-admits it -- + emitter.instruction("movzx r9d, BYTE PTR [rsi]"); // load the subject's first byte + emitter.instruction("cmp r9d, 39"); // is the first byte an apostrophe? + emitter.instruction("jne __rt_str_word_count_first_dash_linux_x86_64"); // check the leading hyphen case instead + emitter.instruction("cmp BYTE PTR [rbp + r9 - 384], 0"); // is the apostrophe an explicitly admitted word character? + emitter.instruction("jne __rt_str_word_count_first_done_linux_x86_64"); // an admitted apostrophe may open a word + emitter.instruction("add rsi, 1"); // skip the leading apostrophe + emitter.instruction("jmp __rt_str_word_count_first_done_linux_x86_64"); // php-src performs at most one leading skip + emitter.label("__rt_str_word_count_first_dash_linux_x86_64"); + emitter.instruction("cmp r9d, 45"); // is the first byte a hyphen? + emitter.instruction("jne __rt_str_word_count_first_done_linux_x86_64"); // any other first byte is scanned normally + emitter.instruction("cmp BYTE PTR [rbp + r9 - 384], 0"); // is the hyphen an explicitly admitted word character? + emitter.instruction("jne __rt_str_word_count_first_done_linux_x86_64"); // an admitted hyphen may open a word + emitter.instruction("add rsi, 1"); // skip the leading hyphen + emitter.label("__rt_str_word_count_first_done_linux_x86_64"); + + // -- php-src drops a trailing `-` unless the character list re-admits it -- + emitter.instruction("movzx r9d, BYTE PTR [rdi - 1]"); // load the subject's last byte + emitter.instruction("cmp r9d, 45"); // is the last byte a hyphen? + emitter.instruction("jne __rt_str_word_count_last_done_linux_x86_64"); // any other last byte stays inside the scan range + emitter.instruction("cmp BYTE PTR [rbp + r9 - 384], 0"); // is the hyphen an explicitly admitted word character? + emitter.instruction("jne __rt_str_word_count_last_done_linux_x86_64"); // an admitted hyphen may close a word + emitter.instruction("sub rdi, 1"); // exclude the trailing hyphen from the scan range + emitter.label("__rt_str_word_count_last_done_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 56], rsi"); // publish the adjusted scan cursor + emitter.instruction("mov QWORD PTR [rbp - 64], rdi"); // publish the adjusted scan end + + // -- walk the subject one candidate word at a time -- + emitter.label("__rt_str_word_count_scan_linux_x86_64"); + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the scan cursor + emitter.instruction("mov rdi, QWORD PTR [rbp - 64]"); // reload the scan end + emitter.instruction("cmp rsi, rdi"); // has the scan reached the adjusted end? + emitter.instruction("jae __rt_str_word_count_done_linux_x86_64"); // every candidate word has been visited + emitter.instruction("mov QWORD PTR [rbp - 80], rsi"); // remember where this candidate word starts + + emitter.label("__rt_str_word_count_word_linux_x86_64"); + emitter.instruction("cmp rsi, rdi"); // is the cursor still inside the scan range? + emitter.instruction("jae __rt_str_word_count_word_end_linux_x86_64"); // the word ends at the scan end + emitter.instruction("movzx r9d, BYTE PTR [rsi]"); // load the candidate word byte + emitter.instruction("mov r10d, r9d"); // copy the byte before folding its case + emitter.instruction("or r10d, 32"); // fold the byte to lower case for the C-locale isalpha() test + emitter.instruction("sub r10d, 97"); // rebase the folded byte on 'a' + emitter.instruction("cmp r10d, 26"); // is the byte an ASCII letter? + emitter.instruction("jb __rt_str_word_count_word_char_linux_x86_64"); // letters always continue the word + emitter.instruction("cmp BYTE PTR [rbp + r9 - 384], 0"); // is the byte an explicitly admitted word character? + emitter.instruction("jne __rt_str_word_count_word_char_linux_x86_64"); // admitted bytes continue the word + emitter.instruction("cmp r9d, 39"); // is the byte an interior apostrophe? + emitter.instruction("je __rt_str_word_count_word_char_linux_x86_64"); // apostrophes always continue the word + emitter.instruction("cmp r9d, 45"); // is the byte an interior hyphen? + emitter.instruction("je __rt_str_word_count_word_char_linux_x86_64"); // hyphens always continue the word + emitter.instruction("jmp __rt_str_word_count_word_end_linux_x86_64"); // any other byte terminates the word + emitter.label("__rt_str_word_count_word_char_linux_x86_64"); + emitter.instruction("add rsi, 1"); // consume the word character + emitter.instruction("jmp __rt_str_word_count_word_linux_x86_64"); // test the next byte + + emitter.label("__rt_str_word_count_word_end_linux_x86_64"); + emitter.instruction("mov QWORD PTR [rbp - 56], rsi"); // publish the cursor before any recording helper call + emitter.instruction("mov r11, rsi"); // copy the cursor before deriving the word length + emitter.instruction("sub r11, QWORD PTR [rbp - 80]"); // how many bytes did this candidate word cover? + emitter.instruction("test r11, r11"); // was the candidate empty? + emitter.instruction("jz __rt_str_word_count_advance_linux_x86_64"); // a zero-length candidate is a separator, not a word + emitter.instruction("mov QWORD PTR [rbp - 88], r11"); // save the word length across the recording helper calls + emitter.instruction("mov r9, QWORD PTR [rbp - 48]"); // reload the requested result format + emitter.instruction("test r9, r9"); // is the caller only counting words? + emitter.instruction("jnz __rt_str_word_count_record_linux_x86_64"); // formats 1 and 2 materialize the word itself + emitter.instruction("add QWORD PTR [rbp - 72], 1"); // count one more word + emitter.instruction("jmp __rt_str_word_count_advance_linux_x86_64"); // move past the terminating byte + + emitter.label("__rt_str_word_count_record_linux_x86_64"); + emitter.instruction("cmp r9, 1"); // is the caller collecting the plain list of words? + emitter.instruction("jne __rt_str_word_count_record_map_linux_x86_64"); // format 2 stores the word under its byte offset + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // reload the result array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 80]"); // pass the word start in the append helper's SysV string-pointer register + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // pass the word length to the append helper + emitter.instruction("call __rt_array_push_str"); // append a persisted copy of the word + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // republish the array pointer after possible growth + emitter.instruction("jmp __rt_str_word_count_advance_linux_x86_64"); // move past the terminating byte + + emitter.label("__rt_str_word_count_record_map_linux_x86_64"); + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the word start + emitter.instruction("sub r10, QWORD PTR [rbp - 32]"); // the map key is the word's byte offset in the subject + emitter.instruction("mov QWORD PTR [rbp - 96], r10"); // save the map key across the persist call + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // pass the word start to the persist helper + emitter.instruction("mov rdx, QWORD PTR [rbp - 88]"); // pass the word length to the persist helper + emitter.instruction("call __rt_str_persist"); // copy the word so the map owns its own bytes + emitter.instruction("mov rcx, rax"); // value_lo = owned word pointer + emitter.instruction("mov r8, rdx"); // value_hi = owned word length + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // reload the result hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 96]"); // key_lo = the word's byte offset + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer key + emitter.instruction("mov r9d, 1"); // runtime tag 1 marks the stored value as a string + emitter.instruction("call __rt_hash_set"); // insert the word under its byte offset + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // republish the hash pointer after possible growth + + emitter.label("__rt_str_word_count_advance_linux_x86_64"); + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the scan cursor + emitter.instruction("add rsi, 1"); // php-src always steps past the terminating byte + emitter.instruction("mov QWORD PTR [rbp - 56], rsi"); // publish the advanced scan cursor + emitter.instruction("jmp __rt_str_word_count_scan_linux_x86_64"); // look for the next candidate word + + emitter.label("__rt_str_word_count_done_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // return the count or the finished container + emitter.instruction("add rsp, 384"); // release the word-scan frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the requested str_word_count result +} diff --git a/src/codegen_support/runtime/strings/stripos.rs b/src/codegen_support/runtime/strings/stripos.rs new file mode 100644 index 0000000000..7cdff9e5c7 --- /dev/null +++ b/src/codegen_support/runtime/strings/stripos.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Emits the `__rt_stripos` runtime helper assembly: the case-insensitive twin of +//! `__rt_strpos`, scanning left to right for the first occurrence of a needle. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The ABI is byte-for-byte identical to `__rt_strpos`, so `lower_string_position` drives +//! both spellings through the same `$offset` normalization, `ValueError` guard, and match +//! rebase. Only the per-byte comparison differs. +//! - Folding is ASCII-only (`A`-`Z` -> `a`-`z`), matching php-src's locale-independent +//! `zend_tolower_ascii`. That is why `stripos("Été", "é")` is 3 rather than 1: the two +//! non-ASCII lead bytes are compared verbatim. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_stripos` runtime helper for AArch64. +/// +/// ABI: +/// Input: x1=haystack_ptr, x2=haystack_len, x3=needle_ptr, x4=needle_len +/// Output: x0 = byte offset of the first case-insensitive match, or -1 when absent +/// +/// An empty needle matches at offset 0 and a needle longer than the haystack cannot match, +/// exactly as `__rt_strpos` decides those two edge cases. +/// Dispatches to `emit_stripos_linux_x86_64` on x86_64. +pub fn emit_stripos(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_stripos_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: stripos ---"); + emitter.label_global("__rt_stripos"); + + // -- edge cases -- + emitter.instruction("cbz x4, __rt_stripos_empty"); // empty needle always matches at position 0 + emitter.instruction("cmp x4, x2"); // compare needle length with haystack length + emitter.instruction("b.gt __rt_stripos_notfound"); // needle longer than haystack, can't match + emitter.instruction("mov x5, #0"); // initialize search position to 0 + + // -- outer loop: try matching needle at each position -- + emitter.label("__rt_stripos_outer"); + emitter.instruction("sub x9, x2, x4"); // last valid start = haystack_len - needle_len + emitter.instruction("cmp x5, x9"); // check if position exceeds last valid start + emitter.instruction("b.gt __rt_stripos_notfound"); // past end, needle not found + + // -- inner loop: compare needle bytes at current position, case-insensitively -- + emitter.instruction("mov x6, #0"); // needle comparison index = 0 + emitter.label("__rt_stripos_inner"); + emitter.instruction("cmp x6, x4"); // check if all needle bytes matched + emitter.instruction("b.ge __rt_stripos_found"); // all matched, found at position x5 + emitter.instruction("add x7, x5, x6"); // compute haystack index = pos + needle_idx + emitter.instruction("ldrb w8, [x1, x7]"); // load haystack byte at computed index + emitter.instruction("ldrb w10, [x3, x6]"); // load needle byte at current index + + // -- fold the haystack byte to lowercase, ASCII range only -- + emitter.instruction("cmp w8, #65"); // is the haystack byte at or above 'A'? + emitter.instruction("b.lt __rt_stripos_fold_needle"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp w8, #90"); // is the haystack byte at or below 'Z'? + emitter.instruction("b.gt __rt_stripos_fold_needle"); // bytes above 'Z' are compared verbatim + emitter.instruction("add w8, w8, #32"); // fold the uppercase haystack byte to lowercase + + // -- fold the needle byte to lowercase, ASCII range only -- + emitter.label("__rt_stripos_fold_needle"); + emitter.instruction("cmp w10, #65"); // is the needle byte at or above 'A'? + emitter.instruction("b.lt __rt_stripos_cmp"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp w10, #90"); // is the needle byte at or below 'Z'? + emitter.instruction("b.gt __rt_stripos_cmp"); // bytes above 'Z' are compared verbatim + emitter.instruction("add w10, w10, #32"); // fold the uppercase needle byte to lowercase + + emitter.label("__rt_stripos_cmp"); + emitter.instruction("cmp w8, w10"); // compare the folded haystack and needle bytes + emitter.instruction("b.ne __rt_stripos_next"); // mismatch, try next position + emitter.instruction("add x6, x6, #1"); // advance needle index + emitter.instruction("b __rt_stripos_inner"); // continue comparing + + // -- advance to next haystack position -- + emitter.label("__rt_stripos_next"); + emitter.instruction("add x5, x5, #1"); // increment search position + emitter.instruction("b __rt_stripos_outer"); // retry from new position + + // -- return results -- + emitter.label("__rt_stripos_found"); + emitter.instruction("mov x0, x5"); // return match position + emitter.instruction("ret"); // return to caller + emitter.label("__rt_stripos_empty"); + emitter.instruction("mov x0, #0"); // empty needle found at position 0 + emitter.instruction("ret"); // return to caller + emitter.label("__rt_stripos_notfound"); + emitter.instruction("mov x0, #-1"); // return -1 (not found) + emitter.instruction("ret"); // return to caller +} + +/// Emits the `__rt_stripos` runtime helper for Linux x86_64. +/// +/// ABI: +/// Input: rdi=haystack_ptr, rsi=haystack_len, rdx=needle_ptr, rcx=needle_len +/// Output: rax = byte offset of the first case-insensitive match, or -1 when absent +/// +/// `rsi` doubles as the needle-byte scratch register once the last valid start offset has +/// been derived from it, the same reuse `__rt_strpos` makes. +/// Called exclusively from `emit_stripos` when `emitter.target.arch == Arch::X86_64`. +fn emit_stripos_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: stripos ---"); + emitter.label_global("__rt_stripos"); + + emitter.instruction("test rcx, rcx"); // empty needles match immediately at offset zero + emitter.instruction("jz __rt_stripos_empty_linux_x86_64"); // return zero when stripos() receives an empty needle + emitter.instruction("cmp rcx, rsi"); // reject searches whose needle is longer than the haystack + emitter.instruction("jg __rt_stripos_notfound_linux_x86_64"); // return the not-found sentinel when the needle cannot fit + emitter.instruction("mov r10, rsi"); // copy the haystack length so the final valid start offset can be computed once + emitter.instruction("sub r10, rcx"); // compute the last haystack offset where the full needle can still fit + emitter.instruction("mov r8, rdi"); // seed the current haystack candidate pointer at the start of the haystack + emitter.instruction("xor r9d, r9d"); // start scanning from haystack offset zero + + emitter.label("__rt_stripos_outer_linux_x86_64"); + emitter.instruction("cmp r9, r10"); // have we advanced beyond the last valid haystack start offset? + emitter.instruction("jg __rt_stripos_notfound_linux_x86_64"); // stop once there are no more candidate start offsets to test + emitter.instruction("xor r11d, r11d"); // start the needle byte comparison from index zero for the current candidate + + emitter.label("__rt_stripos_inner_linux_x86_64"); + emitter.instruction("cmp r11, rcx"); // did every byte in the needle match at the current haystack offset? + emitter.instruction("jge __rt_stripos_found_linux_x86_64"); // return the current haystack offset once the full needle matches + emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // load the current haystack byte for the candidate comparison + emitter.instruction("movzx esi, BYTE PTR [rdx + r11]"); // load the current needle byte for the candidate comparison + emitter.instruction("cmp al, 65"); // is the haystack byte at or above 'A'? + emitter.instruction("jb __rt_stripos_fold_needle_linux_x86_64"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp al, 90"); // is the haystack byte at or below 'Z'? + emitter.instruction("ja __rt_stripos_fold_needle_linux_x86_64"); // bytes above 'Z' are compared verbatim + emitter.instruction("add al, 32"); // fold the uppercase haystack byte to lowercase + + emitter.label("__rt_stripos_fold_needle_linux_x86_64"); + emitter.instruction("cmp sil, 65"); // is the needle byte at or above 'A'? + emitter.instruction("jb __rt_stripos_cmp_linux_x86_64"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp sil, 90"); // is the needle byte at or below 'Z'? + emitter.instruction("ja __rt_stripos_cmp_linux_x86_64"); // bytes above 'Z' are compared verbatim + emitter.instruction("add sil, 32"); // fold the uppercase needle byte to lowercase + + emitter.label("__rt_stripos_cmp_linux_x86_64"); + emitter.instruction("cmp al, sil"); // compare the folded haystack and needle bytes + emitter.instruction("jne __rt_stripos_next_linux_x86_64"); // abandon this candidate start offset on the first mismatching byte + emitter.instruction("add r11, 1"); // advance to the next byte within the current needle comparison + emitter.instruction("jmp __rt_stripos_inner_linux_x86_64"); // continue matching bytes against the current candidate start offset + + emitter.label("__rt_stripos_next_linux_x86_64"); + emitter.instruction("add r8, 1"); // advance the haystack candidate pointer to the next possible start offset + emitter.instruction("add r9, 1"); // advance the logical haystack offset returned on a successful future match + emitter.instruction("jmp __rt_stripos_outer_linux_x86_64"); // retry the needle comparison from the next haystack start offset + + emitter.label("__rt_stripos_found_linux_x86_64"); + emitter.instruction("mov rax, r9"); // return the first haystack offset whose bytes matched the full needle + emitter.instruction("ret"); // return the signed match offset to the caller + + emitter.label("__rt_stripos_empty_linux_x86_64"); + emitter.instruction("xor eax, eax"); // empty needles match at offset zero + emitter.instruction("ret"); // return the empty-needle offset to the caller + + emitter.label("__rt_stripos_notfound_linux_x86_64"); + emitter.instruction("mov rax, -1"); // return the not-found sentinel when no haystack offset matches the needle + emitter.instruction("ret"); // return the not-found sentinel to the caller +} diff --git a/src/codegen_support/runtime/strings/stripslashes.rs b/src/codegen_support/runtime/strings/stripslashes.rs index 81b9d97712..29980e6f0c 100644 --- a/src/codegen_support/runtime/strings/stripslashes.rs +++ b/src/codegen_support/runtime/strings/stripslashes.rs @@ -7,21 +7,25 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - Unescaping never grows the payload, so the source length is reserved through +//! `__rt_concat_reserve` before the first store; inputs beyond the 64 KiB concat scratch +//! buffer fall back to heap storage instead of running off the end of it. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Emits the `__rt_stripslashes` runtime helper for the current target. /// /// Removes escape backslashes from a PHP byte-string by copying bytes from source -/// to the concat buffer, skipping backslashes and their following escaped characters. +/// to reserved destination storage, skipping backslashes and their following escaped characters. /// Trailing backslashes (no character to escape) are preserved as literal backslashes. /// /// # ABI /// - ARM64: input string in x1/x2 (pointer/length), result returned in x1/x2 /// - x86_64 Linux: input string in rax/rdx (pointer/length), result returned in rax/rdx -/// - Updates `_concat_off` and `_concat_buf` with the written output slice +/// - Reserves the (never-exceeded) source length through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`, so only scratch-backed results move `_concat_off`. +/// - Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. pub fn emit_stripslashes(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_stripslashes_linux_x86_64(emitter); @@ -32,11 +36,16 @@ pub fn emit_stripslashes(emitter: &mut Emitter) { emitter.comment("--- runtime: stripslashes ---"); emitter.label_global("__rt_stripslashes"); - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case (unchanged-length) unescaped result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the stripslashes helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x0, x2"); // unescaping never grows the payload, so the source length bounds the result + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the unescaped result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_stripslashes_loop"); @@ -56,24 +65,33 @@ pub fn emit_stripslashes(emitter: &mut Emitter) { emitter.label("__rt_stripslashes_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the stripslashes helper frame emitter.instruction("ret"); // return } /// Emits `__rt_stripslashes` for the x86_64 Linux ABI. /// x86_64 calling convention: input string in rax/rdx, result in rax/rdx. +/// Reserves the (never-exceeded) source length through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`. fn emit_stripslashes_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: stripslashes ---"); emitter.label_global("__rt_stripslashes"); - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // load the current concat-buffer absolute offset before appending the unescaped string - abi::emit_symbol_address(emitter, "r9", "_concat_buf"); // materialize the concat-buffer base pointer for the unescaped string write - emitter.instruction("add r9, r8"); // compute the current concat-buffer write pointer from the base plus offset + // -- reserve the worst-case (unchanged-length) unescaped result before writing anything -- + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("mov rax, rdx"); // unescaping never grows the payload, so the source length bounds the result + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the unescaped result + emitter.instruction("mov r9, rax"); // compute the destination write pointer where the unescaped string begins emitter.instruction("mov r10, r9"); // preserve the unescaped-string start pointer for the final result slice - emitter.instruction("mov rcx, rdx"); // track how many source bytes remain to be scanned for escape prefixes + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // track how many source bytes remain to be scanned for escape prefixes + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the borrowed source cursor the unescape loop advances through emitter.label("__rt_stripslashes_loop"); emitter.instruction("test rcx, rcx"); // have we consumed every byte of the escaped source string? @@ -96,10 +114,10 @@ fn emit_stripslashes_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_stripslashes_done"); emitter.instruction("mov rax, r10"); // return the unescaped-string start pointer in the x86_64 string result pointer register - emitter.instruction("mov rdx, r9"); // snapshot the final concat-buffer write pointer before computing the unescaped result length + emitter.instruction("mov rdx, r9"); // snapshot the final destination write pointer before computing the unescaped result length emitter.instruction("sub rdx, r10"); // compute the unescaped result length from the write pointer minus the start pointer - abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // reload the previous concat-buffer absolute offset before publishing the appended slice - emitter.instruction("add r8, rdx"); // advance the concat-buffer absolute offset by the unescaped result length - abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the updated concat-buffer absolute offset for later writers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the stripslashes spill slots before returning the unescaped string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the unescaped string emitter.instruction("ret"); // return to the caller with the unescaped string slice in rax/rdx } diff --git a/src/codegen_support/runtime/strings/strncasecmp.rs b/src/codegen_support/runtime/strings/strncasecmp.rs new file mode 100644 index 0000000000..1d9e782293 --- /dev/null +++ b/src/codegen_support/runtime/strings/strncasecmp.rs @@ -0,0 +1,141 @@ +//! Purpose: +//! Emits the `__rt_strncasecmp` runtime helper assembly for the PHP `strncasecmp` builtin. +//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - Mirrors php-src's `zend_binary_strncasecmp`: bytes are folded with the ASCII-only +//! `zend_tolower_ascii` before comparison, exactly like `__rt_strcasecmp`, so bytes outside +//! `A`-`Z` are never rewritten and no locale is consulted. +//! - The compared prefix and the equal-prefix tiebreak both use the TRUNCATED lengths +//! `min($length, strlen($a))` / `min($length, strlen($b))`, matching `__rt_strncmp`. +//! - The result is the raw folded-byte difference, not a clamped `-1/0/1`. +//! - `$length` is validated as non-negative by the backend lowering, which raises PHP's +//! catchable `ValueError`; the helper may therefore treat it as an unsigned bound. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_strncasecmp` runtime helper for length-limited case-insensitive comparison. +/// +/// Register contract (AArch64): +/// - Input: `x1` = ptr_a, `x2` = len_a, `x3` = ptr_b, `x4` = len_b, `x5` = compare length +/// - Output: `x0` = result (`< 0` if a < b, `0` if equal, `> 0` if a > b) +/// +/// Register contract (x86_64 System V): +/// - Input: `rdi` = ptr_a, `rsi` = len_a, `rdx` = ptr_b, `rcx` = len_b, `r8` = compare length +/// - Output: `rax` = result (`< 0` if a < b, `0` if equal, `> 0` if a > b) +pub fn emit_strncasecmp(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_strncasecmp_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: strncasecmp ---"); + emitter.label_global("__rt_strncasecmp"); + + // -- truncate both operands to the requested comparison length -- + emitter.instruction("cmp x2, x5"); // compare the first string length against the requested bound + emitter.instruction("csel x9, x2, x5, lo"); // x9 = min(len_a, length), the first effective length + emitter.instruction("cmp x4, x5"); // compare the second string length against the requested bound + emitter.instruction("csel x10, x4, x5, lo"); // x10 = min(len_b, length), the second effective length + emitter.instruction("cmp x9, x10"); // compare both effective lengths + emitter.instruction("csel x11, x9, x10, lo"); // x11 = shared prefix actually compared byte by byte + emitter.instruction("mov x6, #0"); // start comparing at byte offset zero + + emitter.label("__rt_strncasecmp_loop"); + emitter.instruction("cmp x6, x11"); // has the shared prefix been fully compared? + emitter.instruction("b.hs __rt_strncasecmp_len"); // fall back to the effective-length tiebreak + emitter.instruction("ldrb w7, [x1, x6]"); // load the current byte of the first string + emitter.instruction("ldrb w8, [x3, x6]"); // load the current byte of the second string + + // -- ASCII-fold the first string byte -- + emitter.instruction("cmp w7, #65"); // is the first byte at or above 'A'? + emitter.instruction("b.lt __rt_strncasecmp_b"); // bytes below 'A' are compared unchanged + emitter.instruction("cmp w7, #90"); // is the first byte at or below 'Z'? + emitter.instruction("b.gt __rt_strncasecmp_b"); // bytes above 'Z' are compared unchanged + emitter.instruction("add w7, w7, #32"); // fold the uppercase ASCII letter to lowercase + + // -- ASCII-fold the second string byte -- + emitter.label("__rt_strncasecmp_b"); + emitter.instruction("cmp w8, #65"); // is the second byte at or above 'A'? + emitter.instruction("b.lt __rt_strncasecmp_cmp"); // bytes below 'A' are compared unchanged + emitter.instruction("cmp w8, #90"); // is the second byte at or below 'Z'? + emitter.instruction("b.gt __rt_strncasecmp_cmp"); // bytes above 'Z' are compared unchanged + emitter.instruction("add w8, w8, #32"); // fold the uppercase ASCII letter to lowercase + + emitter.label("__rt_strncasecmp_cmp"); + emitter.instruction("cmp w7, w8"); // compare the two folded bytes + emitter.instruction("b.ne __rt_strncasecmp_diff"); // report the byte difference on the first mismatch + emitter.instruction("add x6, x6, #1"); // advance to the next shared-prefix byte + emitter.instruction("b __rt_strncasecmp_loop"); // keep comparing the shared prefix + + emitter.label("__rt_strncasecmp_diff"); + emitter.instruction("sub x0, x7, x8"); // return the signed folded-byte difference + emitter.instruction("ret"); // hand the byte difference back to the caller + + emitter.label("__rt_strncasecmp_len"); + emitter.instruction("sub x0, x9, x10"); // tiebreak on the TRUNCATED lengths, never the raw ones + emitter.instruction("ret"); // hand the length difference back to the caller +} + +/// Emits the x86_64 Linux implementation of `__rt_strncasecmp`. +/// +/// `rsi` and `rcx` are reused as the byte index and second byte scratch once both raw +/// lengths have been truncated into `r9`/`r10`, so the helper needs no callee-saved register. +fn emit_strncasecmp_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strncasecmp ---"); + emitter.label_global("__rt_strncasecmp"); + + // -- truncate both operands to the requested comparison length -- + emitter.instruction("mov r9, rsi"); // seed the first effective length from the first string length + emitter.instruction("cmp r9, r8"); // compare the first string length against the requested bound + emitter.instruction("cmova r9, r8"); // r9 = min(len_a, length), the first effective length + emitter.instruction("mov r10, rcx"); // seed the second effective length from the second string length + emitter.instruction("cmp r10, r8"); // compare the second string length against the requested bound + emitter.instruction("cmova r10, r8"); // r10 = min(len_b, length), the second effective length + emitter.instruction("mov r11, r9"); // seed the shared prefix bound from the first effective length + emitter.instruction("cmp r11, r10"); // compare both effective lengths + emitter.instruction("cmova r11, r10"); // r11 = shared prefix actually compared byte by byte + emitter.instruction("xor esi, esi"); // start comparing at byte offset zero + + emitter.label("__rt_strncasecmp_loop_linux_x86_64"); + emitter.instruction("cmp rsi, r11"); // has the shared prefix been fully compared? + emitter.instruction("jae __rt_strncasecmp_len_linux_x86_64"); // fall back to the effective-length tiebreak + emitter.instruction("movzx rax, BYTE PTR [rdi + rsi]"); // load the current byte of the first string + emitter.instruction("movzx rcx, BYTE PTR [rdx + rsi]"); // load the current byte of the second string + + // -- ASCII-fold the first string byte -- + emitter.instruction("cmp al, 65"); // is the first byte at or above 'A'? + emitter.instruction("jb __rt_strncasecmp_second_linux_x86_64"); // bytes below 'A' are compared unchanged + emitter.instruction("cmp al, 90"); // is the first byte at or below 'Z'? + emitter.instruction("ja __rt_strncasecmp_second_linux_x86_64"); // bytes above 'Z' are compared unchanged + emitter.instruction("add al, 32"); // fold the uppercase ASCII letter to lowercase + + // -- ASCII-fold the second string byte -- + emitter.label("__rt_strncasecmp_second_linux_x86_64"); + emitter.instruction("cmp cl, 65"); // is the second byte at or above 'A'? + emitter.instruction("jb __rt_strncasecmp_cmp_linux_x86_64"); // bytes below 'A' are compared unchanged + emitter.instruction("cmp cl, 90"); // is the second byte at or below 'Z'? + emitter.instruction("ja __rt_strncasecmp_cmp_linux_x86_64"); // bytes above 'Z' are compared unchanged + emitter.instruction("add cl, 32"); // fold the uppercase ASCII letter to lowercase + + emitter.label("__rt_strncasecmp_cmp_linux_x86_64"); + emitter.instruction("cmp rax, rcx"); // compare the two folded bytes + emitter.instruction("jne __rt_strncasecmp_diff_linux_x86_64"); // report the byte difference on the first mismatch + emitter.instruction("add rsi, 1"); // advance to the next shared-prefix byte + emitter.instruction("jmp __rt_strncasecmp_loop_linux_x86_64"); // keep comparing the shared prefix + + emitter.label("__rt_strncasecmp_diff_linux_x86_64"); + emitter.instruction("sub rax, rcx"); // return the signed folded-byte difference + emitter.instruction("ret"); // hand the byte difference back to the caller + + emitter.label("__rt_strncasecmp_len_linux_x86_64"); + emitter.instruction("mov rax, r9"); // seed the tiebreak from the first effective length + emitter.instruction("sub rax, r10"); // tiebreak on the TRUNCATED lengths, never the raw ones + emitter.instruction("ret"); // hand the length difference back to the caller +} diff --git a/src/codegen_support/runtime/strings/strncmp.rs b/src/codegen_support/runtime/strings/strncmp.rs new file mode 100644 index 0000000000..dbad43bf68 --- /dev/null +++ b/src/codegen_support/runtime/strings/strncmp.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! Emits the `__rt_strncmp` runtime helper assembly for the PHP `strncmp` builtin. +//! Keeps PHP byte-string pointer/length behavior and target-specific ABI variants in one focused emitter. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - Mirrors php-src's `zend_binary_strncmp`: the compared prefix is +//! `min($length, min(strlen($a), strlen($b)))`, and when that prefix is equal the result is +//! `min($length, strlen($a)) - min($length, strlen($b))`. Comparing the raw lengths instead +//! would make `strncmp("abc", "ab", 2)` non-zero, which PHP reports as `0`. +//! - The result is the raw byte difference, NOT a clamped `-1/0/1`: reference PHP 8.4 prints +//! `-12` for `strncmp("Hello", "Hexxx", 3)`, matching `__rt_strcmp`'s existing contract. +//! - `$length` is validated as non-negative by the backend lowering, which raises PHP's +//! catchable `ValueError`; the helper may therefore treat it as an unsigned bound. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_strncmp` runtime helper for length-limited string comparison. +/// +/// Register contract (AArch64): +/// - Input: `x1` = ptr_a, `x2` = len_a, `x3` = ptr_b, `x4` = len_b, `x5` = compare length +/// - Output: `x0` = result (`< 0` if a < b, `0` if equal, `> 0` if a > b) +/// +/// Register contract (x86_64 System V): +/// - Input: `rdi` = ptr_a, `rsi` = len_a, `rdx` = ptr_b, `rcx` = len_b, `r8` = compare length +/// - Output: `rax` = result (`< 0` if a < b, `0` if equal, `> 0` if a > b) +/// +/// A zero `$length` truncates both effective lengths to zero, so the helper returns `0` +/// without reading either buffer. +pub fn emit_strncmp(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_strncmp_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: strncmp ---"); + emitter.label_global("__rt_strncmp"); + + // -- truncate both operands to the requested comparison length -- + emitter.instruction("cmp x2, x5"); // compare the first string length against the requested bound + emitter.instruction("csel x9, x2, x5, lo"); // x9 = min(len_a, length), the first effective length + emitter.instruction("cmp x4, x5"); // compare the second string length against the requested bound + emitter.instruction("csel x10, x4, x5, lo"); // x10 = min(len_b, length), the second effective length + emitter.instruction("cmp x9, x10"); // compare both effective lengths + emitter.instruction("csel x11, x9, x10, lo"); // x11 = shared prefix actually compared byte by byte + emitter.instruction("mov x6, #0"); // start comparing at byte offset zero + + emitter.label("__rt_strncmp_loop"); + emitter.instruction("cmp x6, x11"); // has the shared prefix been fully compared? + emitter.instruction("b.hs __rt_strncmp_len"); // fall back to the effective-length tiebreak + emitter.instruction("ldrb w7, [x1, x6]"); // load the current byte of the first string + emitter.instruction("ldrb w8, [x3, x6]"); // load the current byte of the second string + emitter.instruction("cmp w7, w8"); // compare the two bytes + emitter.instruction("b.ne __rt_strncmp_diff"); // report the byte difference on the first mismatch + emitter.instruction("add x6, x6, #1"); // advance to the next shared-prefix byte + emitter.instruction("b __rt_strncmp_loop"); // keep comparing the shared prefix + + emitter.label("__rt_strncmp_diff"); + emitter.instruction("sub x0, x7, x8"); // return the signed byte difference like php-src's memcmp result + emitter.instruction("ret"); // hand the byte difference back to the caller + + emitter.label("__rt_strncmp_len"); + emitter.instruction("sub x0, x9, x10"); // tiebreak on the TRUNCATED lengths, never the raw ones + emitter.instruction("ret"); // hand the length difference back to the caller +} + +/// Emits the x86_64 Linux implementation of `__rt_strncmp`. +/// +/// `rsi` and `rcx` are reused as the byte index and second byte scratch once both raw +/// lengths have been truncated into `r9`/`r10`, so the helper needs no callee-saved register. +fn emit_strncmp_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strncmp ---"); + emitter.label_global("__rt_strncmp"); + + // -- truncate both operands to the requested comparison length -- + emitter.instruction("mov r9, rsi"); // seed the first effective length from the first string length + emitter.instruction("cmp r9, r8"); // compare the first string length against the requested bound + emitter.instruction("cmova r9, r8"); // r9 = min(len_a, length), the first effective length + emitter.instruction("mov r10, rcx"); // seed the second effective length from the second string length + emitter.instruction("cmp r10, r8"); // compare the second string length against the requested bound + emitter.instruction("cmova r10, r8"); // r10 = min(len_b, length), the second effective length + emitter.instruction("mov r11, r9"); // seed the shared prefix bound from the first effective length + emitter.instruction("cmp r11, r10"); // compare both effective lengths + emitter.instruction("cmova r11, r10"); // r11 = shared prefix actually compared byte by byte + emitter.instruction("xor esi, esi"); // start comparing at byte offset zero + + emitter.label("__rt_strncmp_loop_linux_x86_64"); + emitter.instruction("cmp rsi, r11"); // has the shared prefix been fully compared? + emitter.instruction("jae __rt_strncmp_len_linux_x86_64"); // fall back to the effective-length tiebreak + emitter.instruction("movzx rax, BYTE PTR [rdi + rsi]"); // load the current byte of the first string + emitter.instruction("movzx rcx, BYTE PTR [rdx + rsi]"); // load the current byte of the second string + emitter.instruction("cmp rax, rcx"); // compare the two bytes + emitter.instruction("jne __rt_strncmp_diff_linux_x86_64"); // report the byte difference on the first mismatch + emitter.instruction("add rsi, 1"); // advance to the next shared-prefix byte + emitter.instruction("jmp __rt_strncmp_loop_linux_x86_64"); // keep comparing the shared prefix + + emitter.label("__rt_strncmp_diff_linux_x86_64"); + emitter.instruction("sub rax, rcx"); // return the signed byte difference like php-src's memcmp result + emitter.instruction("ret"); // hand the byte difference back to the caller + + emitter.label("__rt_strncmp_len_linux_x86_64"); + emitter.instruction("mov rax, r9"); // seed the tiebreak from the first effective length + emitter.instruction("sub rax, r10"); // tiebreak on the TRUNCATED lengths, never the raw ones + emitter.instruction("ret"); // hand the length difference back to the caller +} diff --git a/src/codegen_support/runtime/strings/strripos.rs b/src/codegen_support/runtime/strings/strripos.rs new file mode 100644 index 0000000000..d67e1aff40 --- /dev/null +++ b/src/codegen_support/runtime/strings/strripos.rs @@ -0,0 +1,158 @@ +//! Purpose: +//! Emits the `__rt_strripos` runtime helper assembly: the case-insensitive twin of +//! `__rt_strrpos`, scanning right to left for the last occurrence of a needle. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The ABI is byte-for-byte identical to `__rt_strrpos`, so `lower_string_position` drives +//! both spellings through the same `$offset` window trimming, `ValueError` guard, and match +//! rebase. Only the per-byte comparison differs. +//! - Folding is ASCII-only (`A`-`Z` -> `a`-`z`), matching php-src's locale-independent +//! `zend_tolower_ascii`; non-ASCII bytes are compared verbatim. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_strripos` runtime helper for AArch64. +/// +/// ABI: +/// Input: x1=haystack_ptr, x2=haystack_len, x3=needle_ptr, x4=needle_len +/// Output: x0 = byte offset of the last case-insensitive match, or -1 when absent +/// +/// An empty needle returns the haystack length (the last valid starting position) and a +/// needle longer than the haystack returns the not-found sentinel immediately, exactly as +/// `__rt_strrpos` decides those two edge cases. +/// Dispatches to `emit_strripos_linux_x86_64` on x86_64. +pub fn emit_strripos(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_strripos_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: strripos ---"); + emitter.label_global("__rt_strripos"); + + // -- edge cases -- + emitter.instruction("cbz x4, __rt_strripos_empty"); // empty needle returns last position + emitter.instruction("cmp x4, x2"); // compare needle length with haystack length + emitter.instruction("b.gt __rt_strripos_notfound"); // needle longer than haystack, can't match + emitter.instruction("sub x5, x2, x4"); // start searching from rightmost valid position + + // -- outer loop: try matching needle from right to left -- + emitter.label("__rt_strripos_outer"); + emitter.instruction("mov x6, #0"); // reset needle comparison index + emitter.label("__rt_strripos_inner"); + emitter.instruction("cmp x6, x4"); // check if all needle bytes matched + emitter.instruction("b.ge __rt_strripos_found"); // all matched, found at position x5 + emitter.instruction("add x7, x5, x6"); // compute haystack index = pos + needle_idx + emitter.instruction("ldrb w8, [x1, x7]"); // load haystack byte at computed index + emitter.instruction("ldrb w10, [x3, x6]"); // load needle byte at current index + + // -- fold the haystack byte to lowercase, ASCII range only -- + emitter.instruction("cmp w8, #65"); // is the haystack byte at or above 'A'? + emitter.instruction("b.lt __rt_strripos_fold_needle"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp w8, #90"); // is the haystack byte at or below 'Z'? + emitter.instruction("b.gt __rt_strripos_fold_needle"); // bytes above 'Z' are compared verbatim + emitter.instruction("add w8, w8, #32"); // fold the uppercase haystack byte to lowercase + + // -- fold the needle byte to lowercase, ASCII range only -- + emitter.label("__rt_strripos_fold_needle"); + emitter.instruction("cmp w10, #65"); // is the needle byte at or above 'A'? + emitter.instruction("b.lt __rt_strripos_cmp"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp w10, #90"); // is the needle byte at or below 'Z'? + emitter.instruction("b.gt __rt_strripos_cmp"); // bytes above 'Z' are compared verbatim + emitter.instruction("add w10, w10, #32"); // fold the uppercase needle byte to lowercase + + emitter.label("__rt_strripos_cmp"); + emitter.instruction("cmp w8, w10"); // compare the folded haystack and needle bytes + emitter.instruction("b.ne __rt_strripos_prev"); // mismatch, try previous position + emitter.instruction("add x6, x6, #1"); // advance needle index + emitter.instruction("b __rt_strripos_inner"); // continue comparing + + // -- move to previous position (searching right to left) -- + emitter.label("__rt_strripos_prev"); + emitter.instruction("cbz x5, __rt_strripos_notfound"); // if at position 0, nowhere left to search + emitter.instruction("sub x5, x5, #1"); // decrement search position + emitter.instruction("b __rt_strripos_outer"); // retry from new position + + // -- return results -- + emitter.label("__rt_strripos_found"); + emitter.instruction("mov x0, x5"); // return last match position + emitter.instruction("ret"); // return to caller + emitter.label("__rt_strripos_empty"); + emitter.instruction("mov x0, x2"); // empty needle returns haystack length + emitter.instruction("ret"); // return to caller + emitter.label("__rt_strripos_notfound"); + emitter.instruction("mov x0, #-1"); // return -1 (not found) + emitter.instruction("ret"); // return to caller +} + +/// Emits the `__rt_strripos` runtime helper for Linux x86_64. +/// +/// ABI: +/// Input: rdi=haystack_ptr, rsi=haystack_len, rdx=needle_ptr, rcx=needle_len +/// Output: rax = byte offset of the last case-insensitive match, or -1 when absent +/// +/// Called exclusively from `emit_strripos` when `emitter.target.arch == Arch::X86_64`. +fn emit_strripos_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strripos ---"); + emitter.label_global("__rt_strripos"); + + emitter.instruction("test rcx, rcx"); // empty needles match just after the last haystack byte + emitter.instruction("jz __rt_strripos_empty_linux_x86_64"); // return the haystack length when strripos() receives an empty needle + emitter.instruction("cmp rcx, rsi"); // reject searches whose needle is longer than the haystack + emitter.instruction("jg __rt_strripos_notfound_linux_x86_64"); // return the not-found sentinel when the needle cannot fit + emitter.instruction("mov r9, rsi"); // copy the haystack length so the rightmost valid start offset can be computed once + emitter.instruction("sub r9, rcx"); // compute the rightmost haystack offset where the full needle can still fit + + emitter.label("__rt_strripos_outer_linux_x86_64"); + emitter.instruction("xor r10d, r10d"); // start the needle byte comparison from index zero for the current candidate + + emitter.label("__rt_strripos_inner_linux_x86_64"); + emitter.instruction("cmp r10, rcx"); // did every byte in the needle match at the current haystack offset? + emitter.instruction("jge __rt_strripos_found_linux_x86_64"); // return the current haystack offset once the full needle matches + emitter.instruction("mov r8, r9"); // copy the current candidate start offset so the indexed haystack byte can be addressed + emitter.instruction("add r8, r10"); // compute the absolute haystack byte offset for the current needle byte + emitter.instruction("movzx eax, BYTE PTR [rdi + r8]"); // load the current haystack byte for the right-to-left candidate comparison + emitter.instruction("movzx r11d, BYTE PTR [rdx + r10]"); // load the current needle byte for the right-to-left candidate comparison + emitter.instruction("cmp al, 65"); // is the haystack byte at or above 'A'? + emitter.instruction("jb __rt_strripos_fold_needle_linux_x86_64"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp al, 90"); // is the haystack byte at or below 'Z'? + emitter.instruction("ja __rt_strripos_fold_needle_linux_x86_64"); // bytes above 'Z' are compared verbatim + emitter.instruction("add al, 32"); // fold the uppercase haystack byte to lowercase + + emitter.label("__rt_strripos_fold_needle_linux_x86_64"); + emitter.instruction("cmp r11b, 65"); // is the needle byte at or above 'A'? + emitter.instruction("jb __rt_strripos_cmp_linux_x86_64"); // bytes below 'A' are compared verbatim + emitter.instruction("cmp r11b, 90"); // is the needle byte at or below 'Z'? + emitter.instruction("ja __rt_strripos_cmp_linux_x86_64"); // bytes above 'Z' are compared verbatim + emitter.instruction("add r11b, 32"); // fold the uppercase needle byte to lowercase + + emitter.label("__rt_strripos_cmp_linux_x86_64"); + emitter.instruction("cmp al, r11b"); // compare the folded haystack and needle bytes + emitter.instruction("jne __rt_strripos_prev_linux_x86_64"); // abandon this candidate start offset on the first mismatching byte + emitter.instruction("add r10, 1"); // advance to the next byte within the current needle comparison + emitter.instruction("jmp __rt_strripos_inner_linux_x86_64"); // continue matching bytes against the current right-to-left candidate start offset + + emitter.label("__rt_strripos_prev_linux_x86_64"); + emitter.instruction("test r9, r9"); // are we already at haystack offset zero with no further candidates left to test? + emitter.instruction("jz __rt_strripos_notfound_linux_x86_64"); // return the not-found sentinel once the final candidate also mismatches + emitter.instruction("sub r9, 1"); // move the candidate start offset one byte to the left + emitter.instruction("jmp __rt_strripos_outer_linux_x86_64"); // retry the needle comparison from the next right-to-left haystack start offset + + emitter.label("__rt_strripos_found_linux_x86_64"); + emitter.instruction("mov rax, r9"); // return the last haystack offset whose bytes matched the full needle + emitter.instruction("ret"); // return the signed match offset to the caller + + emitter.label("__rt_strripos_empty_linux_x86_64"); + emitter.instruction("mov rax, rsi"); // empty needles match just after the final haystack byte + emitter.instruction("ret"); // return the empty-needle offset to the caller + + emitter.label("__rt_strripos_notfound_linux_x86_64"); + emitter.instruction("mov rax, -1"); // return the not-found sentinel when no haystack offset matches the needle + emitter.instruction("ret"); // return the not-found sentinel to the caller +} diff --git a/src/codegen_support/runtime/strings/strtr.rs b/src/codegen_support/runtime/strings/strtr.rs new file mode 100644 index 0000000000..177d0c42fd --- /dev/null +++ b/src/codegen_support/runtime/strings/strtr.rs @@ -0,0 +1,800 @@ +//! Purpose: +//! Emits the `strtr()` runtime helpers: `__rt_strtr_pairwise` for the three-argument byte +//! translation form, and `__rt_strtr_hash` / `__rt_strtr_array` plus the shared +//! `__rt_strtr_probe` and `__rt_strtr_int_key_len` for the replacement-pair form. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The pairwise form builds a 256-byte translation table (identity, then `$from[i] -> +//! $to[i]` for `i < min(len($from), len($to))`, last mapping wins) and rewrites the subject +//! byte by byte, so the result is always exactly as long as the subject. +//! - The pair form reproduces php-src's single left-to-right pass with longest-match-first +//! selection and no re-substitution: at each position the longest key length that still +//! fits is probed down to the shortest, the replacement is copied verbatim, and scanning +//! resumes after the MATCHED key. Keys shorter than one byte or longer than the whole +//! subject are ignored, exactly as php-src ignores them. +//! - Integer keys are matched through `__rt_hash_normalize_key`, which maps the probed +//! substring back onto the canonical integer key the hash actually stores, so +//! `strtr("12345", [1 => "one"])` behaves like php-src. +//! - The replacement result length is not known up front, so the pair form runs the match +//! loop twice: once to size the result exactly and once to write it. That keeps the +//! `__rt_concat_reserve` reservation exact instead of appending unbounded into the shared +//! scratch buffer. +//! - Every result is copied into owned heap storage by `__rt_str_persist` and the superseded +//! reservation is released through `__rt_heap_free_safe`, which matches the `Fresh` +//! ownership contract on `RuntimeFnId::Strtr` and keeps results larger than the 64 KiB +//! scratch buffer from leaking their heap fallback block. +//! - php-src also emits `Warning: strtr(): Ignoring replacement of empty string` for a +//! zero-length key. elephc skips the key with the same observable result but does not +//! emit that warning. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits every `strtr()` runtime helper for the active target. +pub fn emit_strtr(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_strtr_int_key_len_x86_64(emitter); + emit_strtr_probe_x86_64(emitter); + emit_strtr_pairwise_x86_64(emitter); + emit_strtr_hash_x86_64(emitter); + emit_strtr_array_x86_64(emitter); + return; + } + emit_strtr_int_key_len_aarch64(emitter); + emit_strtr_probe_aarch64(emitter); + emit_strtr_pairwise_aarch64(emitter); + emit_strtr_hash_aarch64(emitter); + emit_strtr_array_aarch64(emitter); +} + +/// Emits the AArch64 `__rt_strtr_int_key_len` helper. +/// +/// Returns how many bytes php-src would use to spell one integer array key, so an integer +/// key participates in the same longest-match-first length window as a string key. +/// +/// - Input: `x0` = integer key. +/// - Output: `x0` = decimal digit count, plus one for a negative sign. +/// - Leaf routine: no frame, no calls. +fn emit_strtr_int_key_len_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_int_key_len ---"); + emitter.label_global("__rt_strtr_int_key_len"); + + emitter.instruction("mov x9, #0"); // positive keys spend no byte on a sign + emitter.instruction("cmp x0, #0"); // is this a negative integer key? + emitter.instruction("b.ge __rt_strtr_int_key_len_magnitude"); // positive keys are already their own magnitude + emitter.instruction("neg x0, x0"); // measure the magnitude of a negative key + emitter.instruction("mov x9, #1"); // a negative key also spells a leading '-' + + emitter.label("__rt_strtr_int_key_len_magnitude"); + emitter.instruction("mov x10, #1"); // every integer spells at least one digit + emitter.instruction("mov x11, #10"); // decimal radix + + emitter.label("__rt_strtr_int_key_len_loop"); + emitter.instruction("cmp x0, #10"); // is there another decimal digit left? + emitter.instruction("b.lo __rt_strtr_int_key_len_done"); // a value below ten has no further digits + emitter.instruction("udiv x0, x0, x11"); // drop the lowest decimal digit + emitter.instruction("add x10, x10, #1"); // count the dropped digit + emitter.instruction("b __rt_strtr_int_key_len_loop"); // keep counting decimal digits + + emitter.label("__rt_strtr_int_key_len_done"); + emitter.instruction("add x0, x10, x9"); // total spelled length = digits + optional sign + emitter.instruction("ret"); // return the spelled key length +} + +/// Emits the AArch64 `__rt_strtr_probe` helper. +/// +/// Finds php-src's longest matching replacement key at one subject position. +/// +/// - Input: `x0` = pairs hash, `x1` = position pointer, `x2` = remaining bytes, +/// `x3` = shortest usable key length, `x4` = longest usable key length. +/// - Output: `x0` = matched key length (`0` when nothing matched), `x1` = replacement +/// pointer, `x2` = replacement length. +fn emit_strtr_probe_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_probe ---"); + emitter.label_global("__rt_strtr_probe"); + + emitter.instruction("sub sp, sp, #64"); // allocate the probe frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save the frame pointer and return address across the lookup calls + emitter.instruction("add x29, sp, #48"); // establish the probe helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the pairs hash across the lookup calls + emitter.instruction("str x1, [sp, #8]"); // save the probed subject position + emitter.instruction("str x3, [sp, #16]"); // save the shortest usable key length + emitter.instruction("cmp x4, x2"); // does the longest key still fit the remaining subject? + emitter.instruction("csel x5, x2, x4, hi"); // clamp the first probed length to what remains + emitter.instruction("str x5, [sp, #24]"); // save the current candidate key length + + emitter.label("__rt_strtr_probe_loop"); + emitter.instruction("ldr x5, [sp, #24]"); // reload the current candidate key length + emitter.instruction("ldr x3, [sp, #16]"); // reload the shortest usable key length + emitter.instruction("cmp x5, x3"); // have all candidate lengths been tried? + emitter.instruction("b.lo __rt_strtr_probe_miss"); // nothing matches at this subject position + emitter.instruction("ldr x1, [sp, #8]"); // the candidate substring starts at the probed position + emitter.instruction("mov x2, x5"); // the candidate substring is as long as the current length + emitter.instruction("bl __rt_hash_normalize_key"); // map numeric substrings onto the integer keys the hash stores + emitter.instruction("ldr x0, [sp, #0]"); // reload the pairs hash for the lookup + emitter.instruction("bl __rt_hash_get"); // x0 = found, x1 = replacement pointer, x2 = replacement length + emitter.instruction("cbnz x0, __rt_strtr_probe_hit"); // the longest matching key wins + emitter.instruction("ldr x5, [sp, #24]"); // reload the current candidate key length + emitter.instruction("sub x5, x5, #1"); // try the next shorter key length + emitter.instruction("str x5, [sp, #24]"); // publish the shortened candidate length + emitter.instruction("b __rt_strtr_probe_loop"); // keep probing shorter keys + + emitter.label("__rt_strtr_probe_hit"); + emitter.instruction("ldr x0, [sp, #24]"); // report the matched key length without disturbing the replacement pair + emitter.instruction("b __rt_strtr_probe_done"); // the probe is finished + + emitter.label("__rt_strtr_probe_miss"); + emitter.instruction("mov x0, xzr"); // report that no replacement key matched here + emitter.instruction("mov x1, xzr"); // no replacement pointer for a miss + emitter.instruction("mov x2, xzr"); // no replacement length for a miss + + emitter.label("__rt_strtr_probe_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the probe frame + emitter.instruction("ret"); // return the probe outcome +} + +/// Emits the AArch64 `__rt_strtr_pairwise` helper for `strtr($string, $from, $to)`. +/// +/// - Input: `x1`/`x2` = subject, `x3`/`x4` = `$from`, `x5`/`x6` = `$to`. +/// - Output: `x1`/`x2` = owned translated string. +fn emit_strtr_pairwise_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_pairwise ---"); + emitter.label_global("__rt_strtr_pairwise"); + + // Frame layout (320 bytes): + // [sp, #0] = 256-byte byte translation table + // [sp, #256] = subject pointer + // [sp, #264] = subject length + // [sp, #272] = reservation start + // [sp, #280] = owned result pointer + // [sp, #288] = owned result length + // [sp, #304] = saved x29/x30 + emitter.instruction("sub sp, sp, #320"); // allocate the translation frame + emitter.instruction("stp x29, x30, [sp, #304]"); // save the frame pointer and return address across the reservation calls + emitter.instruction("add x29, sp, #304"); // establish the pairwise helper frame pointer + emitter.instruction("str x1, [sp, #256]"); // save the subject pointer across the reservation call + emitter.instruction("str x2, [sp, #264]"); // save the subject length across the reservation call + + // -- seed the translation table with the identity mapping -- + emitter.instruction("mov x10, sp"); // x10 = translation table base + emitter.instruction("mov x9, #0"); // start at byte value zero + emitter.label("__rt_strtr_pairwise_identity"); + emitter.instruction("strb w9, [x10, x9]"); // an unmapped byte translates to itself + emitter.instruction("add x9, x9, #1"); // advance to the next byte value + emitter.instruction("cmp x9, #256"); // is the identity table complete? + emitter.instruction("b.lo __rt_strtr_pairwise_identity"); // keep seeding the identity mapping + + // -- overwrite the mapped bytes; php-src truncates to the shorter of the two lists -- + emitter.instruction("cmp x4, x6"); // which of $from and $to is shorter? + emitter.instruction("csel x7, x6, x4, hi"); // the mapping covers only min(len($from), len($to)) bytes + emitter.instruction("mov x9, #0"); // start at the first mapped pair + emitter.label("__rt_strtr_pairwise_map"); + emitter.instruction("cmp x9, x7"); // has the whole mapping been applied? + emitter.instruction("b.hs __rt_strtr_pairwise_map_done"); // the translation table is complete + emitter.instruction("ldrb w11, [x3, x9]"); // load the source byte of this pair + emitter.instruction("ldrb w12, [x5, x9]"); // load the destination byte of this pair + emitter.instruction("strb w12, [x10, x11]"); // a later pair for the same source byte wins, as in php-src + emitter.instruction("add x9, x9, #1"); // advance to the next mapped pair + emitter.instruction("b __rt_strtr_pairwise_map"); // keep applying the mapping + emitter.label("__rt_strtr_pairwise_map_done"); + + // -- the result is always exactly as long as the subject -- + emitter.instruction("ldr x0, [sp, #264]"); // request exactly the subject byte count + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the translated result + emitter.instruction("str x0, [sp, #272]"); // save the reservation start + emitter.instruction("mov x13, x0"); // destination cursor + emitter.instruction("mov x10, sp"); // restore the translation table base after the reservation call + emitter.instruction("ldr x1, [sp, #256]"); // reload the borrowed subject pointer + emitter.instruction("ldr x2, [sp, #264]"); // reload the subject length + emitter.instruction("mov x9, #0"); // start translating at the first subject byte + + emitter.label("__rt_strtr_pairwise_translate"); + emitter.instruction("cmp x9, x2"); // has the whole subject been translated? + emitter.instruction("b.hs __rt_strtr_pairwise_translate_done"); // the translated result is complete + emitter.instruction("ldrb w11, [x1, x9]"); // load the next subject byte + emitter.instruction("ldrb w12, [x10, x11]"); // look up its translation + emitter.instruction("strb w12, [x13, x9]"); // store the translated byte at the same offset + emitter.instruction("add x9, x9, #1"); // advance to the next subject byte + emitter.instruction("b __rt_strtr_pairwise_translate"); // keep translating subject bytes + + emitter.label("__rt_strtr_pairwise_translate_done"); + emitter.instruction("ldr x1, [sp, #272]"); // the translated result starts at the reservation + emitter.instruction("ldr x2, [sp, #264]"); // the translated result is as long as the subject + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("bl __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("str x1, [sp, #280]"); // save the owned result pointer across the reservation release + emitter.instruction("str x2, [sp, #288]"); // save the owned result length across the reservation release + emitter.instruction("ldr x0, [sp, #272]"); // reload the superseded reservation + emitter.instruction("bl __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("ldr x1, [sp, #280]"); // restore the owned result pointer + emitter.instruction("ldr x2, [sp, #288]"); // restore the owned result length + emitter.instruction("ldp x29, x30, [sp, #304]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #320"); // release the translation frame + emitter.instruction("ret"); // return the translated string as a PHP string pair +} + +/// Emits the AArch64 `__rt_strtr_hash` helper for `strtr($string, $pairs)`. +/// +/// - Input: `x0` = pairs hash, `x1`/`x2` = subject. +/// - Output: `x1`/`x2` = owned replaced string. +fn emit_strtr_hash_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_hash ---"); + emitter.label_global("__rt_strtr_hash"); + + // Frame layout (128 bytes): + // [sp, #0] = pairs hash + // [sp, #8] = subject pointer + // [sp, #16] = subject length + // [sp, #24] = shortest usable key length + // [sp, #32] = longest usable key length (0 = no usable key at all) + // [sp, #40] = hash iteration cursor + // [sp, #48] = measured result length, then the destination cursor + // [sp, #56] = reservation start + // [sp, #64] = scan position inside the subject + // [sp, #72] = owned result pointer + // [sp, #80] = owned result length + // [sp, #112] = saved x29/x30 + emitter.instruction("sub sp, sp, #128"); // allocate the replacement frame + emitter.instruction("stp x29, x30, [sp, #112]"); // save the frame pointer and return address across the helper calls + emitter.instruction("add x29, sp, #112"); // establish the pair-form helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the pairs hash + emitter.instruction("str x1, [sp, #8]"); // save the subject pointer + emitter.instruction("str x2, [sp, #16]"); // save the subject length + emitter.instruction("str xzr, [sp, #40]"); // start the hash walk from the head entry + emitter.instruction("mov x9, #-1"); // seed the shortest key length with the largest possible value + emitter.instruction("lsr x9, x9, #1"); // PHP_INT_MAX is the neutral element for the minimum + emitter.instruction("str x9, [sp, #24]"); // publish the seeded shortest key length + emitter.instruction("str xzr, [sp, #32]"); // no usable key has been seen yet + + // -- measure the usable key-length window php-src probes at every position -- + emitter.label("__rt_strtr_hash_keys"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the pairs hash for the walk + emitter.instruction("ldr x1, [sp, #40]"); // reload the insertion-order cursor + emitter.instruction("bl __rt_hash_iter_next"); // x0 = next cursor, x1 = key payload, x2 = key length + emitter.instruction("cmn x0, #1"); // did the iterator signal end-of-walk? + emitter.instruction("b.eq __rt_strtr_hash_keys_done"); // the key-length window is complete + emitter.instruction("str x0, [sp, #40]"); // save the next insertion-order cursor + emitter.instruction("cmn x2, #1"); // is this an inline integer key? + emitter.instruction("b.ne __rt_strtr_hash_key_len"); // string keys already carry their byte length + emitter.instruction("mov x0, x1"); // measure how php-src would spell this integer key + emitter.instruction("bl __rt_strtr_int_key_len"); // x0 = spelled key length + emitter.instruction("mov x2, x0"); // treat the spelled length as this key's length + + emitter.label("__rt_strtr_hash_key_len"); + emitter.instruction("cmp x2, #1"); // is the key at least one byte long? + emitter.instruction("b.lt __rt_strtr_hash_keys"); // php-src ignores an empty replacement key + emitter.instruction("ldr x9, [sp, #16]"); // reload the subject length + emitter.instruction("cmp x2, x9"); // could this key ever fit inside the subject? + emitter.instruction("b.hi __rt_strtr_hash_keys"); // php-src skips keys longer than the whole subject + emitter.instruction("ldr x10, [sp, #24]"); // reload the shortest usable key length + emitter.instruction("cmp x2, x10"); // is this key shorter than every key seen so far? + emitter.instruction("csel x10, x2, x10, lo"); // keep the shortest usable key length + emitter.instruction("str x10, [sp, #24]"); // publish the shortest usable key length + emitter.instruction("ldr x11, [sp, #32]"); // reload the longest usable key length + emitter.instruction("cmp x2, x11"); // is this key longer than every key seen so far? + emitter.instruction("csel x11, x2, x11, hi"); // keep the longest usable key length + emitter.instruction("str x11, [sp, #32]"); // publish the longest usable key length + emitter.instruction("b __rt_strtr_hash_keys"); // consider the next replacement key + emitter.label("__rt_strtr_hash_keys_done"); + + // -- first pass: size the result exactly so the reservation stays bounded -- + emitter.instruction("str xzr, [sp, #48]"); // the measured result starts empty + emitter.instruction("str xzr, [sp, #64]"); // start measuring at the first subject byte + + emitter.label("__rt_strtr_hash_measure"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("ldr x10, [sp, #16]"); // reload the subject length + emitter.instruction("cmp x9, x10"); // has the whole subject been measured? + emitter.instruction("b.hs __rt_strtr_hash_measure_done"); // the exact result size is known + emitter.instruction("ldr x11, [sp, #32]"); // reload the longest usable key length + emitter.instruction("cbz x11, __rt_strtr_hash_measure_plain"); // with no usable key every byte is copied verbatim + emitter.instruction("ldr x0, [sp, #0]"); // pass the pairs hash to the probe + emitter.instruction("ldr x1, [sp, #8]"); // reload the subject base + emitter.instruction("add x1, x1, x9"); // probe from the current scan position + emitter.instruction("sub x2, x10, x9"); // tell the probe how many subject bytes remain + emitter.instruction("ldr x3, [sp, #24]"); // pass the shortest usable key length + emitter.instruction("mov x4, x11"); // pass the longest usable key length + emitter.instruction("bl __rt_strtr_probe"); // x0 = matched key length, x2 = replacement length + emitter.instruction("cbz x0, __rt_strtr_hash_measure_plain"); // no key matched here, so this byte is copied verbatim + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("add x9, x9, x0"); // php-src resumes after the matched key, never re-substituting + emitter.instruction("str x9, [sp, #64]"); // publish the advanced scan position + emitter.instruction("ldr x10, [sp, #48]"); // reload the measured result length + emitter.instruction("adds x10, x10, x2"); // the replacement contributes its own length + emitter.instruction("b.cs __rt_strtr_hash_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("str x10, [sp, #48]"); // publish the measured result length + emitter.instruction("b __rt_strtr_hash_measure"); // measure the next subject position + + emitter.label("__rt_strtr_hash_measure_plain"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("add x9, x9, #1"); // an unmatched byte advances the scan by one + emitter.instruction("str x9, [sp, #64]"); // publish the advanced scan position + emitter.instruction("ldr x10, [sp, #48]"); // reload the measured result length + emitter.instruction("adds x10, x10, #1"); // an unmatched byte contributes one byte + emitter.instruction("b.cs __rt_strtr_hash_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("str x10, [sp, #48]"); // publish the measured result length + emitter.instruction("b __rt_strtr_hash_measure"); // measure the next subject position + emitter.label("__rt_strtr_hash_measure_done"); + + emitter.instruction("ldr x0, [sp, #48]"); // request exactly the measured result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the replaced result + emitter.instruction("str x0, [sp, #56]"); // save the reservation start + emitter.instruction("str x0, [sp, #48]"); // the destination cursor starts at the reservation + emitter.instruction("str xzr, [sp, #64]"); // restart the scan for the writing pass + + // -- second pass: replay the same matches, writing into the exact reservation -- + emitter.label("__rt_strtr_hash_write"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("ldr x10, [sp, #16]"); // reload the subject length + emitter.instruction("cmp x9, x10"); // has the whole subject been rewritten? + emitter.instruction("b.hs __rt_strtr_hash_write_done"); // the replaced result is complete + emitter.instruction("ldr x11, [sp, #32]"); // reload the longest usable key length + emitter.instruction("cbz x11, __rt_strtr_hash_write_plain"); // with no usable key every byte is copied verbatim + emitter.instruction("ldr x0, [sp, #0]"); // pass the pairs hash to the probe + emitter.instruction("ldr x1, [sp, #8]"); // reload the subject base + emitter.instruction("add x1, x1, x9"); // probe from the current scan position + emitter.instruction("sub x2, x10, x9"); // tell the probe how many subject bytes remain + emitter.instruction("ldr x3, [sp, #24]"); // pass the shortest usable key length + emitter.instruction("mov x4, x11"); // pass the longest usable key length + emitter.instruction("bl __rt_strtr_probe"); // x0 = matched key length, x1/x2 = replacement pair + emitter.instruction("cbz x0, __rt_strtr_hash_write_plain"); // no key matched here, so this byte is copied verbatim + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("add x9, x9, x0"); // php-src resumes after the matched key, never re-substituting + emitter.instruction("str x9, [sp, #64]"); // publish the advanced scan position + emitter.instruction("ldr x12, [sp, #48]"); // reload the destination cursor + emitter.instruction("mov x13, #0"); // start copying at the first replacement byte + + emitter.label("__rt_strtr_hash_copy"); + emitter.instruction("cmp x13, x2"); // has the whole replacement been copied? + emitter.instruction("b.hs __rt_strtr_hash_copy_done"); // the replacement is fully written + emitter.instruction("ldrb w14, [x1, x13]"); // load the next replacement byte + emitter.instruction("strb w14, [x12, x13]"); // store it at the same offset inside the result + emitter.instruction("add x13, x13, #1"); // advance the copy index + emitter.instruction("b __rt_strtr_hash_copy"); // copy the next replacement byte + + emitter.label("__rt_strtr_hash_copy_done"); + emitter.instruction("add x12, x12, x2"); // advance the destination cursor past the replacement + emitter.instruction("str x12, [sp, #48]"); // publish the advanced destination cursor + emitter.instruction("b __rt_strtr_hash_write"); // rewrite the next subject position + + emitter.label("__rt_strtr_hash_write_plain"); + emitter.instruction("ldr x9, [sp, #64]"); // reload the scan position + emitter.instruction("ldr x1, [sp, #8]"); // reload the subject base clobbered by the probe + emitter.instruction("ldrb w14, [x1, x9]"); // load the unmatched subject byte + emitter.instruction("ldr x12, [sp, #48]"); // reload the destination cursor + emitter.instruction("strb w14, [x12]"); // copy the unmatched byte verbatim + emitter.instruction("add x12, x12, #1"); // advance the destination cursor by one byte + emitter.instruction("str x12, [sp, #48]"); // publish the advanced destination cursor + emitter.instruction("add x9, x9, #1"); // an unmatched byte advances the scan by one + emitter.instruction("str x9, [sp, #64]"); // publish the advanced scan position + emitter.instruction("b __rt_strtr_hash_write"); // rewrite the next subject position + + emitter.label("__rt_strtr_hash_write_done"); + emitter.instruction("ldr x1, [sp, #56]"); // the replaced result starts at the reservation + emitter.instruction("ldr x12, [sp, #48]"); // reload the final destination cursor + emitter.instruction("sub x2, x12, x1"); // the written byte count is the result length + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("bl __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("str x1, [sp, #72]"); // save the owned result pointer across the reservation release + emitter.instruction("str x2, [sp, #80]"); // save the owned result length across the reservation release + emitter.instruction("ldr x0, [sp, #56]"); // reload the superseded reservation + emitter.instruction("bl __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("ldr x1, [sp, #72]"); // restore the owned result pointer + emitter.instruction("ldr x2, [sp, #80]"); // restore the owned result length + emitter.instruction("ldp x29, x30, [sp, #112]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #128"); // release the replacement frame + emitter.instruction("ret"); // return the replaced string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_strtr_hash_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe +} + +/// Emits the AArch64 `__rt_strtr_array` helper for an indexed-array `$pairs` argument. +/// +/// php-src treats an indexed array as the pair list `{"0": e0, "1": e1, ...}`, so the array +/// is converted into an owned temporary hash, replaced through `__rt_strtr_hash`, and the +/// temporary is released once the result has been copied into its own storage. +/// +/// - Input: `x0` = indexed array, `x1`/`x2` = subject. +/// - Output: `x1`/`x2` = owned replaced string. +fn emit_strtr_array_aarch64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_array ---"); + emitter.label_global("__rt_strtr_array"); + + emitter.instruction("sub sp, sp, #48"); // allocate the conversion frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save the frame pointer and return address across the helper calls + emitter.instruction("add x29, sp, #32"); // establish the indexed-pairs helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the borrowed subject across the conversion + emitter.instruction("bl __rt_array_to_hash"); // build the owned {0: e0, 1: e1, ...} pair hash + emitter.instruction("str x0, [sp, #16]"); // save the temporary pair hash for release + emitter.instruction("ldp x1, x2, [sp]"); // restore the borrowed subject + emitter.instruction("bl __rt_strtr_hash"); // run the ordinary pair-form replacement + emitter.instruction("stp x1, x2, [sp]"); // save the owned result across the temporary release + emitter.instruction("ldr x0, [sp, #16]"); // reload the temporary pair hash + emitter.instruction("bl __rt_hash_free_deep"); // release the temporary pair hash and its persisted values + emitter.instruction("ldp x1, x2, [sp]"); // restore the owned result + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the conversion frame + emitter.instruction("ret"); // return the replaced string as a PHP string pair +} + +/// Emits the x86_64 `__rt_strtr_int_key_len` helper. +/// +/// - Input: `rax` = integer key. +/// - Output: `rax` = decimal digit count, plus one for a negative sign. +fn emit_strtr_int_key_len_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_int_key_len ---"); + emitter.label_global("__rt_strtr_int_key_len"); + + emitter.instruction("xor r9d, r9d"); // positive keys spend no byte on a sign + emitter.instruction("test rax, rax"); // is this a negative integer key? + emitter.instruction("jge __rt_strtr_int_key_len_magnitude_x86"); // positive keys are already their own magnitude + emitter.instruction("neg rax"); // measure the magnitude of a negative key + emitter.instruction("mov r9d, 1"); // a negative key also spells a leading '-' + + emitter.label("__rt_strtr_int_key_len_magnitude_x86"); + emitter.instruction("mov r10d, 1"); // every integer spells at least one digit + emitter.instruction("mov r11, 10"); // decimal radix + + emitter.label("__rt_strtr_int_key_len_loop_x86"); + emitter.instruction("cmp rax, 10"); // is there another decimal digit left? + emitter.instruction("jb __rt_strtr_int_key_len_done_x86"); // a value below ten has no further digits + emitter.instruction("xor edx, edx"); // clear the dividend high half before the unsigned division + emitter.instruction("div r11"); // drop the lowest decimal digit + emitter.instruction("add r10, 1"); // count the dropped digit + emitter.instruction("jmp __rt_strtr_int_key_len_loop_x86"); // keep counting decimal digits + + emitter.label("__rt_strtr_int_key_len_done_x86"); + emitter.instruction("mov rax, r10"); // total spelled length starts at the digit count + emitter.instruction("add rax, r9"); // add the optional sign byte + emitter.instruction("ret"); // return the spelled key length +} + +/// Emits the x86_64 `__rt_strtr_probe` helper. +/// +/// - Input: `rdi` = pairs hash, `rsi` = position pointer, `rdx` = remaining bytes, +/// `rcx` = shortest usable key length, `r8` = longest usable key length. +/// - Output: `rax` = matched key length (`0` when nothing matched), `rdi` = replacement +/// pointer, `rsi` = replacement length. +fn emit_strtr_probe_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_probe ---"); + emitter.label_global("__rt_strtr_probe"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the lookup calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the probe state + emitter.instruction("sub rsp, 64"); // reserve aligned spill slots for the probe state + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the pairs hash across the lookup calls + emitter.instruction("mov QWORD PTR [rbp - 40], rsi"); // save the probed subject position + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the shortest usable key length + emitter.instruction("mov r9, r8"); // start from the longest usable key length + emitter.instruction("cmp r9, rdx"); // does the longest key still fit the remaining subject? + emitter.instruction("cmova r9, rdx"); // clamp the first probed length to what remains + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the current candidate key length + + emitter.label("__rt_strtr_probe_loop_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate key length + emitter.instruction("cmp r9, QWORD PTR [rbp - 48]"); // have all candidate lengths been tried? + emitter.instruction("jb __rt_strtr_probe_miss_x86"); // nothing matches at this subject position + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // the candidate substring starts at the probed position + emitter.instruction("mov rdx, r9"); // the candidate substring is as long as the current length + emitter.instruction("call __rt_hash_normalize_key"); // map numeric substrings onto the integer keys the hash stores + emitter.instruction("mov rsi, rax"); // move the normalized key_lo into the lookup register + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the pairs hash for the lookup + emitter.instruction("call __rt_hash_get"); // rax = found, rdi = replacement pointer, rsi = replacement length + emitter.instruction("test rax, rax"); // did this candidate length match a replacement key? + emitter.instruction("jnz __rt_strtr_probe_hit_x86"); // the longest matching key wins + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate key length + emitter.instruction("sub r9, 1"); // try the next shorter key length + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // publish the shortened candidate length + emitter.instruction("jmp __rt_strtr_probe_loop_x86"); // keep probing shorter keys + + emitter.label("__rt_strtr_probe_hit_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // report the matched key length without disturbing the replacement pair + emitter.instruction("jmp __rt_strtr_probe_done_x86"); // the probe is finished + + emitter.label("__rt_strtr_probe_miss_x86"); + emitter.instruction("xor eax, eax"); // report that no replacement key matched here + emitter.instruction("xor edi, edi"); // no replacement pointer for a miss + emitter.instruction("xor esi, esi"); // no replacement length for a miss + + emitter.label("__rt_strtr_probe_done_x86"); + emitter.instruction("add rsp, 64"); // release the probe frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the probe outcome +} + +/// Emits the x86_64 `__rt_strtr_pairwise` helper for `strtr($string, $from, $to)`. +/// +/// - Input: `rax`/`rdx` = subject, `rdi`/`rsi` = `$from`, `rcx`/`r8` = `$to`. +/// - Output: `rax`/`rdx` = owned translated string. +fn emit_strtr_pairwise_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_pairwise ---"); + emitter.label_global("__rt_strtr_pairwise"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the translation table + emitter.instruction("sub rsp, 320"); // reserve the saved-argument slots plus the 256-byte translation table + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the subject pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the subject length across the reservation call + + // -- seed the translation table with the identity mapping -- + emitter.instruction("xor r9d, r9d"); // start at byte value zero + emitter.label("__rt_strtr_pairwise_identity_x86"); + emitter.instruction("mov BYTE PTR [rbp + r9 - 320], r9b"); // an unmapped byte translates to itself + emitter.instruction("add r9, 1"); // advance to the next byte value + emitter.instruction("cmp r9, 256"); // is the identity table complete? + emitter.instruction("jb __rt_strtr_pairwise_identity_x86"); // keep seeding the identity mapping + + // -- overwrite the mapped bytes; php-src truncates to the shorter of the two lists -- + emitter.instruction("mov r10, rsi"); // start from the $from byte count + emitter.instruction("cmp r10, r8"); // which of $from and $to is shorter? + emitter.instruction("cmova r10, r8"); // the mapping covers only min(len($from), len($to)) bytes + emitter.instruction("xor r9d, r9d"); // start at the first mapped pair + emitter.label("__rt_strtr_pairwise_map_x86"); + emitter.instruction("cmp r9, r10"); // has the whole mapping been applied? + emitter.instruction("jae __rt_strtr_pairwise_map_done_x86"); // the translation table is complete + emitter.instruction("movzx r11d, BYTE PTR [rdi + r9]"); // load the source byte of this pair + emitter.instruction("movzx eax, BYTE PTR [rcx + r9]"); // load the destination byte of this pair + emitter.instruction("mov BYTE PTR [rbp + r11 - 320], al"); // a later pair for the same source byte wins, as in php-src + emitter.instruction("add r9, 1"); // advance to the next mapped pair + emitter.instruction("jmp __rt_strtr_pairwise_map_x86"); // keep applying the mapping + emitter.label("__rt_strtr_pairwise_map_done_x86"); + + // -- the result is always exactly as long as the subject -- + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // request exactly the subject byte count + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the translated result + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the reservation start + emitter.instruction("mov rsi, rax"); // destination cursor + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the borrowed subject pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // reload the subject length + emitter.instruction("xor r9d, r9d"); // start translating at the first subject byte + + emitter.label("__rt_strtr_pairwise_translate_x86"); + emitter.instruction("cmp r9, rcx"); // has the whole subject been translated? + emitter.instruction("jae __rt_strtr_pairwise_translate_done_x86"); // the translated result is complete + emitter.instruction("movzx r10d, BYTE PTR [rdi + r9]"); // load the next subject byte + emitter.instruction("mov r11b, BYTE PTR [rbp + r10 - 320]"); // look up its translation + emitter.instruction("mov BYTE PTR [rsi + r9], r11b"); // store the translated byte at the same offset + emitter.instruction("add r9, 1"); // advance to the next subject byte + emitter.instruction("jmp __rt_strtr_pairwise_translate_x86"); // keep translating subject bytes + + emitter.label("__rt_strtr_pairwise_translate_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // the translated result starts at the reservation + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // the translated result is as long as the subject + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("call __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the owned result pointer across the reservation release + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the owned result length across the reservation release + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the superseded reservation + emitter.instruction("call __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // restore the owned result pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // restore the owned result length + emitter.instruction("add rsp, 320"); // release the translation frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the translated string as a PHP string pair +} + +/// Emits the x86_64 `__rt_strtr_hash` helper for `strtr($string, $pairs)`. +/// +/// - Input: `rdi` = pairs hash, `rax`/`rdx` = subject. +/// - Output: `rax`/`rdx` = owned replaced string. +fn emit_strtr_hash_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_hash ---"); + emitter.label_global("__rt_strtr_hash"); + + // Frame layout: + // [rbp - 32] = pairs hash + // [rbp - 40] = subject pointer + // [rbp - 48] = subject length + // [rbp - 56] = shortest usable key length + // [rbp - 64] = longest usable key length (0 = no usable key at all) + // [rbp - 72] = hash iteration cursor + // [rbp - 80] = measured result length, then the destination cursor + // [rbp - 88] = reservation start + // [rbp - 96] = scan position inside the subject + // [rbp - 104] = owned result pointer + // [rbp - 112] = owned result length + emitter.instruction("push rbp"); // preserve the caller frame pointer across the helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the replacement state + emitter.instruction("sub rsp, 112"); // reserve aligned spill slots for the replacement state + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the pairs hash + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the subject pointer + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the subject length + emitter.instruction("mov QWORD PTR [rbp - 72], 0"); // start the hash walk from the head entry + emitter.instruction("mov r9, 0x7fffffffffffffff"); // seed the shortest key length with PHP_INT_MAX + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // publish the seeded shortest key length + emitter.instruction("mov QWORD PTR [rbp - 64], 0"); // no usable key has been seen yet + + // -- measure the usable key-length window php-src probes at every position -- + emitter.label("__rt_strtr_hash_keys_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the pairs hash for the walk + emitter.instruction("mov rsi, QWORD PTR [rbp - 72]"); // reload the insertion-order cursor + emitter.instruction("call __rt_hash_iter_next"); // rax = next cursor, rdi = key payload, rdx = key length + emitter.instruction("cmp rax, -1"); // did the iterator signal end-of-walk? + emitter.instruction("je __rt_strtr_hash_keys_done_x86"); // the key-length window is complete + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the next insertion-order cursor + emitter.instruction("cmp rdx, -1"); // is this an inline integer key? + emitter.instruction("jne __rt_strtr_hash_key_len_x86"); // string keys already carry their byte length + emitter.instruction("mov rax, rdi"); // measure how php-src would spell this integer key + emitter.instruction("call __rt_strtr_int_key_len"); // rax = spelled key length + emitter.instruction("mov rdx, rax"); // treat the spelled length as this key's length + + emitter.label("__rt_strtr_hash_key_len_x86"); + emitter.instruction("cmp rdx, 1"); // is the key at least one byte long? + emitter.instruction("jl __rt_strtr_hash_keys_x86"); // php-src ignores an empty replacement key + emitter.instruction("cmp rdx, QWORD PTR [rbp - 48]"); // could this key ever fit inside the subject? + emitter.instruction("ja __rt_strtr_hash_keys_x86"); // php-src skips keys longer than the whole subject + emitter.instruction("mov r10, QWORD PTR [rbp - 56]"); // reload the shortest usable key length + emitter.instruction("cmp rdx, r10"); // is this key shorter than every key seen so far? + emitter.instruction("cmovb r10, rdx"); // keep the shortest usable key length + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // publish the shortest usable key length + emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the longest usable key length + emitter.instruction("cmp rdx, r11"); // is this key longer than every key seen so far? + emitter.instruction("cmova r11, rdx"); // keep the longest usable key length + emitter.instruction("mov QWORD PTR [rbp - 64], r11"); // publish the longest usable key length + emitter.instruction("jmp __rt_strtr_hash_keys_x86"); // consider the next replacement key + emitter.label("__rt_strtr_hash_keys_done_x86"); + + // -- first pass: size the result exactly so the reservation stays bounded -- + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // the measured result starts empty + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // start measuring at the first subject byte + + emitter.label("__rt_strtr_hash_measure_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the subject length + emitter.instruction("cmp r9, r10"); // has the whole subject been measured? + emitter.instruction("jae __rt_strtr_hash_measure_done_x86"); // the exact result size is known + emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the longest usable key length + emitter.instruction("test r11, r11"); // is there any usable replacement key at all? + emitter.instruction("jz __rt_strtr_hash_measure_plain_x86"); // with no usable key every byte is copied verbatim + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the pairs hash to the probe + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reload the subject base + emitter.instruction("add rsi, r9"); // probe from the current scan position + emitter.instruction("mov rdx, r10"); // copy the subject length before deriving what remains + emitter.instruction("sub rdx, r9"); // tell the probe how many subject bytes remain + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // pass the shortest usable key length + emitter.instruction("mov r8, r11"); // pass the longest usable key length + emitter.instruction("call __rt_strtr_probe"); // rax = matched key length, rsi = replacement length + emitter.instruction("test rax, rax"); // did any replacement key match here? + emitter.instruction("jz __rt_strtr_hash_measure_plain_x86"); // no key matched here, so this byte is copied verbatim + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("add r9, rax"); // php-src resumes after the matched key, never re-substituting + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // publish the advanced scan position + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the measured result length + emitter.instruction("add r10, rsi"); // the replacement contributes its own length + emitter.instruction("jc __rt_strtr_hash_overflow_x86"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // publish the measured result length + emitter.instruction("jmp __rt_strtr_hash_measure_x86"); // measure the next subject position + + emitter.label("__rt_strtr_hash_measure_plain_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("add r9, 1"); // an unmatched byte advances the scan by one + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // publish the advanced scan position + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the measured result length + emitter.instruction("add r10, 1"); // an unmatched byte contributes one byte + emitter.instruction("jc __rt_strtr_hash_overflow_x86"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // publish the measured result length + emitter.instruction("jmp __rt_strtr_hash_measure_x86"); // measure the next subject position + emitter.label("__rt_strtr_hash_measure_done_x86"); + + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // request exactly the measured result size + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the replaced result + emitter.instruction("mov QWORD PTR [rbp - 88], rax"); // save the reservation start + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // the destination cursor starts at the reservation + emitter.instruction("mov QWORD PTR [rbp - 96], 0"); // restart the scan for the writing pass + + // -- second pass: replay the same matches, writing into the exact reservation -- + emitter.label("__rt_strtr_hash_write_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the subject length + emitter.instruction("cmp r9, r10"); // has the whole subject been rewritten? + emitter.instruction("jae __rt_strtr_hash_write_done_x86"); // the replaced result is complete + emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the longest usable key length + emitter.instruction("test r11, r11"); // is there any usable replacement key at all? + emitter.instruction("jz __rt_strtr_hash_write_plain_x86"); // with no usable key every byte is copied verbatim + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the pairs hash to the probe + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reload the subject base + emitter.instruction("add rsi, r9"); // probe from the current scan position + emitter.instruction("mov rdx, r10"); // copy the subject length before deriving what remains + emitter.instruction("sub rdx, r9"); // tell the probe how many subject bytes remain + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // pass the shortest usable key length + emitter.instruction("mov r8, r11"); // pass the longest usable key length + emitter.instruction("call __rt_strtr_probe"); // rax = matched key length, rdi/rsi = replacement pair + emitter.instruction("test rax, rax"); // did any replacement key match here? + emitter.instruction("jz __rt_strtr_hash_write_plain_x86"); // no key matched here, so this byte is copied verbatim + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("add r9, rax"); // php-src resumes after the matched key, never re-substituting + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // publish the advanced scan position + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the destination cursor + emitter.instruction("xor r11d, r11d"); // start copying at the first replacement byte + + emitter.label("__rt_strtr_hash_copy_x86"); + emitter.instruction("cmp r11, rsi"); // has the whole replacement been copied? + emitter.instruction("jae __rt_strtr_hash_copy_done_x86"); // the replacement is fully written + emitter.instruction("mov cl, BYTE PTR [rdi + r11]"); // load the next replacement byte + emitter.instruction("mov BYTE PTR [r10 + r11], cl"); // store it at the same offset inside the result + emitter.instruction("add r11, 1"); // advance the copy index + emitter.instruction("jmp __rt_strtr_hash_copy_x86"); // copy the next replacement byte + + emitter.label("__rt_strtr_hash_copy_done_x86"); + emitter.instruction("add r10, rsi"); // advance the destination cursor past the replacement + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // publish the advanced destination cursor + emitter.instruction("jmp __rt_strtr_hash_write_x86"); // rewrite the next subject position + + emitter.label("__rt_strtr_hash_write_plain_x86"); + emitter.instruction("mov r9, QWORD PTR [rbp - 96]"); // reload the scan position + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the subject base clobbered by the probe + emitter.instruction("mov cl, BYTE PTR [rdi + r9]"); // load the unmatched subject byte + emitter.instruction("mov r10, QWORD PTR [rbp - 80]"); // reload the destination cursor + emitter.instruction("mov BYTE PTR [r10], cl"); // copy the unmatched byte verbatim + emitter.instruction("add r10, 1"); // advance the destination cursor by one byte + emitter.instruction("mov QWORD PTR [rbp - 80], r10"); // publish the advanced destination cursor + emitter.instruction("add r9, 1"); // an unmatched byte advances the scan by one + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // publish the advanced scan position + emitter.instruction("jmp __rt_strtr_hash_write_x86"); // rewrite the next subject position + + emitter.label("__rt_strtr_hash_write_done_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // the replaced result starts at the reservation + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the final destination cursor + emitter.instruction("sub rdx, rax"); // the written byte count is the result length + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("call __rt_str_persist"); // hand back owned heap storage, matching the Fresh ownership contract + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save the owned result pointer across the reservation release + emitter.instruction("mov QWORD PTR [rbp - 112], rdx"); // save the owned result length across the reservation release + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the superseded reservation + emitter.instruction("call __rt_heap_free_safe"); // release a heap-backed reservation; concat-scratch pointers are skipped + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // restore the owned result pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 112]"); // restore the owned result length + emitter.instruction("add rsp, 112"); // release the replacement frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the replaced string as a PHP string pair + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_strtr_hash_overflow_x86"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller +} + +/// Emits the x86_64 `__rt_strtr_array` helper for an indexed-array `$pairs` argument. +/// +/// - Input: `rdi` = indexed array, `rax`/`rdx` = subject. +/// - Output: `rax`/`rdx` = owned replaced string. +fn emit_strtr_array_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: strtr_array ---"); + emitter.label_global("__rt_strtr_array"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer across the helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed subject + emitter.instruction("sub rsp, 48"); // reserve aligned spill slots for the subject and temporary hash + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the borrowed subject pointer across the conversion + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the borrowed subject length across the conversion + emitter.instruction("call __rt_array_to_hash"); // build the owned {0: e0, 1: e1, ...} pair hash + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the temporary pair hash for release + emitter.instruction("mov rdi, rax"); // pass the temporary pair hash to the replacement helper + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // restore the borrowed subject pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // restore the borrowed subject length + emitter.instruction("call __rt_strtr_hash"); // run the ordinary pair-form replacement + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the owned result pointer across the temporary release + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the owned result length across the temporary release + emitter.instruction("mov rax, QWORD PTR [rbp - 48]"); // reload the temporary pair hash + emitter.instruction("call __rt_hash_free_deep"); // release the temporary pair hash and its persisted values + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // restore the owned result pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // restore the owned result length + emitter.instruction("add rsp, 48"); // release the conversion frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the replaced string as a PHP string pair +} diff --git a/src/codegen_support/runtime/strings/substr_count.rs b/src/codegen_support/runtime/strings/substr_count.rs new file mode 100644 index 0000000000..ceb8169353 --- /dev/null +++ b/src/codegen_support/runtime/strings/substr_count.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Emits the `__rt_substr_count` runtime helper assembly for the PHP `substr_count` builtin. +//! Counts non-overlapping needle occurrences inside an already-sliced haystack window. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::strings`. +//! +//! Key details: +//! - The helper receives the WINDOW, not the original subject: `substr_count()`'s `$offset` +//! and `$length` are normalized (and their `ValueError`s raised) in the backend lowering, +//! which then passes `haystack + offset` and the clamped length. That keeps the catchable +//! diagnostics out of the runtime, where a fatal could not be caught. +//! - Matching is NON-OVERLAPPING, exactly like php-src: a hit advances the cursor by the full +//! needle length, so `substr_count("aaaa", "aa")` is 2 rather than 3. +//! - The helper allocates nothing and calls nothing, so it needs no frame and no concat +//! scratch reservation. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +/// Emits the `__rt_substr_count` runtime helper for the `substr_count` builtin. +/// +/// ABI (AArch64): +/// Input: `x1` = window pointer, `x2` = window length, `x3` = needle pointer, +/// `x4` = needle length. +/// Output: `x0` = number of non-overlapping matches. +/// +/// ABI (x86_64 System V): +/// Input: `rdi` = window pointer, `rsi` = window length, `rdx` = needle pointer, +/// `rcx` = needle length. +/// Output: `rax` = number of non-overlapping matches. +/// +/// An empty needle and a needle longer than the window both yield zero; the empty case +/// never reaches the helper because the lowering raises PHP's `ValueError` first, but the +/// guard keeps the loop from spinning if it ever did. +pub fn emit_substr_count(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_substr_count_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: substr_count ---"); + emitter.label_global("__rt_substr_count"); + + emitter.instruction("mov x0, #0"); // start the match counter at zero + emitter.instruction("cbz x4, __rt_substr_count_done"); // an empty needle can never be counted + emitter.instruction("cmp x4, x2"); // compare the needle length against the searchable window + emitter.instruction("b.gt __rt_substr_count_done"); // a needle longer than the window cannot match + emitter.instruction("sub x10, x2, x4"); // compute the last window offset where the needle still fits + emitter.instruction("mov x5, #0"); // start scanning at window offset zero + + emitter.label("__rt_substr_count_outer"); + emitter.instruction("cmp x5, x10"); // has the cursor passed the last candidate start offset? + emitter.instruction("b.gt __rt_substr_count_done"); // stop once no full needle can start here + emitter.instruction("mov x6, #0"); // restart the needle comparison at byte zero + + emitter.label("__rt_substr_count_inner"); + emitter.instruction("cmp x6, x4"); // did every needle byte match at this candidate offset? + emitter.instruction("b.hs __rt_substr_count_hit"); // a complete needle match was found + emitter.instruction("add x7, x5, x6"); // compute the window index of the byte under comparison + emitter.instruction("ldrb w9, [x1, x7]"); // load the window byte at the candidate position + emitter.instruction("ldrb w11, [x3, x6]"); // load the needle byte at the same relative position + emitter.instruction("cmp w9, w11"); // compare the window and needle bytes + emitter.instruction("b.ne __rt_substr_count_next"); // abandon this candidate on the first mismatch + emitter.instruction("add x6, x6, #1"); // advance to the next needle byte + emitter.instruction("b __rt_substr_count_inner"); // keep comparing the current candidate + + emitter.label("__rt_substr_count_hit"); + emitter.instruction("add x0, x0, #1"); // record one more non-overlapping match + emitter.instruction("add x5, x5, x4"); // skip the whole matched needle so matches never overlap + emitter.instruction("b __rt_substr_count_outer"); // resume scanning after the counted match + + emitter.label("__rt_substr_count_next"); + emitter.instruction("add x5, x5, #1"); // slide the candidate start one byte forward + emitter.instruction("b __rt_substr_count_outer"); // retry the match at the next window offset + + emitter.label("__rt_substr_count_done"); + emitter.instruction("ret"); // return the accumulated match count +} + +/// Emits `__rt_substr_count` for x86_64 Linux using the System V ABI. +/// +/// `rsi` is repurposed as the last valid start offset once the window length has been used, +/// which frees `r11` for the per-candidate window pointer and leaves `rax` as the only byte +/// scratch register the inner comparison needs. +fn emit_substr_count_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: substr_count ---"); + emitter.label_global("__rt_substr_count"); + + emitter.instruction("xor r10d, r10d"); // start the match counter at zero + emitter.instruction("test rcx, rcx"); // is the needle empty? + emitter.instruction("jz __rt_substr_count_done_linux_x86_64"); // an empty needle can never be counted + emitter.instruction("cmp rcx, rsi"); // compare the needle length against the searchable window + emitter.instruction("jg __rt_substr_count_done_linux_x86_64"); // a needle longer than the window cannot match + emitter.instruction("sub rsi, rcx"); // reuse the window length as the last valid start offset + emitter.instruction("xor r8d, r8d"); // start scanning at window offset zero + + emitter.label("__rt_substr_count_outer_linux_x86_64"); + emitter.instruction("cmp r8, rsi"); // has the cursor passed the last candidate start offset? + emitter.instruction("jg __rt_substr_count_done_linux_x86_64"); // stop once no full needle can start here + emitter.instruction("lea r11, [rdi + r8]"); // point at the window bytes for the current candidate + emitter.instruction("xor r9d, r9d"); // restart the needle comparison at byte zero + + emitter.label("__rt_substr_count_inner_linux_x86_64"); + emitter.instruction("cmp r9, rcx"); // did every needle byte match at this candidate offset? + emitter.instruction("jae __rt_substr_count_hit_linux_x86_64"); // a complete needle match was found + emitter.instruction("movzx eax, BYTE PTR [r11 + r9]"); // load the window byte at the candidate position + emitter.instruction("cmp al, BYTE PTR [rdx + r9]"); // compare it against the needle byte at the same position + emitter.instruction("jne __rt_substr_count_next_linux_x86_64"); // abandon this candidate on the first mismatch + emitter.instruction("add r9, 1"); // advance to the next needle byte + emitter.instruction("jmp __rt_substr_count_inner_linux_x86_64"); // keep comparing the current candidate + + emitter.label("__rt_substr_count_hit_linux_x86_64"); + emitter.instruction("add r10, 1"); // record one more non-overlapping match + emitter.instruction("add r8, rcx"); // skip the whole matched needle so matches never overlap + emitter.instruction("jmp __rt_substr_count_outer_linux_x86_64"); // resume scanning after the counted match + + emitter.label("__rt_substr_count_next_linux_x86_64"); + emitter.instruction("add r8, 1"); // slide the candidate start one byte forward + emitter.instruction("jmp __rt_substr_count_outer_linux_x86_64"); // retry the match at the next window offset + + emitter.label("__rt_substr_count_done_linux_x86_64"); + emitter.instruction("mov rax, r10"); // return the accumulated match count + emitter.instruction("ret"); // hand the count back to the caller +} diff --git a/src/codegen_support/runtime/strings/substr_replace.rs b/src/codegen_support/runtime/strings/substr_replace.rs index a53221adca..913bd87599 100644 --- a/src/codegen_support/runtime/strings/substr_replace.rs +++ b/src/codegen_support/runtime/strings/substr_replace.rs @@ -7,6 +7,9 @@ //! //! Key details: //! - String helpers scan or transform byte ranges and return target ABI pointer/length pairs for generated call sites. +//! - The result can never exceed `subject_len + replacement_len`, and that bound is reserved +//! through `__rt_concat_reserve` before the first store, so long subjects or replacements fall +//! back to heap storage instead of running off the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -27,8 +30,12 @@ use crate::codegen_support::platform::Arch; /// tail-relative index; if still negative it is clamped to 0. /// 2. Expands length=-1 to "remaining bytes from offset". Clamps negative lengths to 0. /// 3. Clamps the slice end to subject_len. -/// 4. Builds result in concat buffer as: prefix (subject[0..offset]) -/// + replacement + suffix (subject[slice_end..]) +/// 4. Builds the result in storage reserved through `__rt_concat_reserve` as: +/// prefix (subject[0..offset]) + replacement + suffix (subject[slice_end..]), +/// then publishes the written length through `__rt_concat_publish`. +/// +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. +/// A wrapped `subject_len + replacement_len` bound reports PHP's allocation-overflow fatal. pub fn emit_substr_replace(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_substr_replace_linux_x86_64(emitter); @@ -38,9 +45,9 @@ pub fn emit_substr_replace(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: substr_replace ---"); emitter.label_global("__rt_substr_replace"); - emitter.instruction("sub sp, sp, #16"); // allocate stack frame - emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address - emitter.instruction("mov x29, sp"); // set frame pointer + emitter.instruction("sub sp, sp, #64"); // allocate stack frame with spill slots for the clamped bounds and both input strings + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // set frame pointer // -- clamp offset -- emitter.instruction("cmp x0, #0"); // check if offset is negative @@ -63,12 +70,20 @@ pub fn emit_substr_replace(emitter: &mut Emitter) { emitter.instruction("cmp x8, x2"); // clamp end to string length emitter.instruction("csel x8, x2, x8, gt"); // min(end, len) + // -- reserve the exact upper bound (subject + replacement) before writing anything -- + emitter.instruction("stp x0, x8, [sp, #0]"); // save the clamped replacement offset and slice end across the reservation call + emitter.instruction("stp x1, x2, [sp, #16]"); // save the subject pointer and length across the reservation call + emitter.instruction("stp x3, x4, [sp, #32]"); // save the replacement pointer and length across the reservation call + emitter.instruction("adds x0, x2, x4"); // the result can never exceed subject length plus replacement length + emitter.instruction("b.cs __rt_subrepl_size_overflow"); // reject a wrapped bound instead of reserving a too-small destination + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov x12, x0"); // destination pointer + emitter.instruction("mov x13, x0"); // save result start + emitter.instruction("ldp x0, x8, [sp, #0]"); // reload the clamped replacement offset and slice end + emitter.instruction("ldp x1, x2, [sp, #16]"); // reload the subject pointer and length + emitter.instruction("ldp x3, x4, [sp, #32]"); // reload the replacement pointer and length + // -- build result: prefix + replacement + suffix -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x12, x11, x10"); // destination pointer - emitter.instruction("mov x13, x12"); // save result start // -- copy prefix: subject[0..offset] -- emitter.instruction("mov x14, #0"); // copy index @@ -105,12 +120,14 @@ pub fn emit_substr_replace(emitter: &mut Emitter) { emitter.label("__rt_subrepl_done"); emitter.instruction("mov x1, x13"); // result pointer emitter.instruction("sub x2, x12, x13"); // result length - emitter.instruction("ldr x10, [x9]"); // reload current offset - emitter.instruction("add x10, x10, x2"); // advance by result length - emitter.instruction("str x10, [x9]"); // store updated offset - emitter.instruction("ldp x29, x30, [sp]"); // restore frame - emitter.instruction("add sp, sp, #16"); // deallocate + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame + emitter.instruction("add sp, sp, #64"); // deallocate emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_subrepl_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux variant of `__rt_substr_replace`. @@ -164,12 +181,14 @@ fn emit_substr_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmovg r11, rcx"); // clamp the suffix start to the end of the subject string when the slice overruns emitter.instruction("mov QWORD PTR [rbp - 40], r9"); // preserve the clamped replacement offset for the prefix copy loop emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // preserve the clamped suffix start for the suffix copy loop - crate::codegen_support::abi::emit_symbol_address(emitter, "rcx", "_concat_off"); - emitter.instruction("mov r8, QWORD PTR [rcx]"); // load the current concat-buffer write offset before emitting the replacement result - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r8, [r10 + r8]"); // compute the concat-buffer destination pointer where the replaced string begins + + // -- reserve the exact upper bound (subject + replacement) before writing anything -- + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // seed the reservation bound from the subject-string length + emitter.instruction("add rax, QWORD PTR [rbp - 32]"); // the result can never exceed subject length plus replacement length + emitter.instruction("jc __rt_substr_replace_size_overflow_linux_x86_64"); // reject a wrapped bound instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the replaced string + emitter.instruction("mov r8, rax"); // compute the destination pointer where the replaced string begins emitter.instruction("mov QWORD PTR [rbp - 56], r8"); // preserve the replaced-string start pointer for the final x86_64 string return pair - emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // preserve the concat-offset symbol address so the helper can publish the new write position emitter.instruction("xor rcx, rcx"); // start the prefix copy loop from byte offset zero emitter.label("__rt_substr_replace_prefix_linux_x86_64"); @@ -209,14 +228,15 @@ fn emit_substr_replace_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_substr_replace_suffix_loop_linux_x86_64"); // continue copying suffix bytes until the subject-string end is reached emitter.label("__rt_substr_replace_done_linux_x86_64"); - emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the concat-buffer start pointer of the replaced string in the primary x86_64 string result register - emitter.instruction("mov rdx, r8"); // copy the concat-buffer end pointer so the final replaced-string length can be derived - emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the concat-buffer start/end pointers - emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the concat-offset symbol address before publishing the new write position - emitter.instruction("mov r9, QWORD PTR [rcx]"); // reload the old concat-buffer write offset before advancing it by the replaced-string length - emitter.instruction("add r9, rdx"); // advance the concat-buffer write offset by the emitted replaced-string length - emitter.instruction("mov QWORD PTR [rcx], r9"); // publish the updated concat-buffer write offset after emitting the replaced string + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // return the reserved start pointer of the replaced string in the primary x86_64 string result register + emitter.instruction("mov rdx, r8"); // copy the destination end pointer so the final replaced-string length can be derived + emitter.instruction("sub rdx, rax"); // derive the replaced-string length from the destination start/end pointers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results emitter.instruction("add rsp, 64"); // release the substr_replace() spill slots before returning the replaced string emitter.instruction("pop rbp"); // restore the caller frame pointer before returning to the caller emitter.instruction("ret"); // return the replaced string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_substr_replace_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/ucwords.rs b/src/codegen_support/runtime/strings/ucwords.rs index 63a75d55f1..24c4402ad2 100644 --- a/src/codegen_support/runtime/strings/ucwords.rs +++ b/src/codegen_support/runtime/strings/ucwords.rs @@ -7,23 +7,30 @@ //! //! Key details: //! - String helpers use PHP pointer/length pairs and target ABI return registers; heap-backed results must remain refcount-compatible. +//! - PHP's `$separators` is a byte SET, so the helper takes a bounded separator string and +//! tests each subject byte for membership. The caller always supplies one: the backend +//! passes `_ucwords_default_seps` (`" \t\r\n\f\v"`) when the argument is omitted, so the +//! default and an explicit set share this single scan. The previous hard-coded +//! space/tab/newline test also silently failed PHP's `\r`, `\f`, and `\v` defaults. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; /// Emits the `__rt_ucwords` runtime helper for ARM64. /// -/// Uppercases the first character of each word in a PHP byte-string. -/// Whitespace characters (space ASCII 32, tab ASCII 9, newline ASCII 10) are word separators. +/// Uppercases the first character of each word in a PHP byte-string, where a word starts at +/// the subject's first byte and after every byte that appears in the separator set. /// /// Input registers (ARM64): /// - x1: pointer to the input string /// - x2: length of the input string +/// - x3: pointer to the separator byte set +/// - x4: length of the separator byte set /// Output registers: -/// - x1: pointer to the result (heap-allocated via `__rt_strcopy`, refcounted) +/// - x1: pointer to the result (concat-backed mutable copy via `__rt_strcopy`) /// - x2: length of the result string /// -/// Clobbers: x9, x10, x11, x12. +/// Clobbers: x9, x10, x11, x12, x13, x14, x15. pub fn emit_ucwords(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_ucwords_linux_x86_64(emitter); @@ -33,10 +40,12 @@ pub fn emit_ucwords(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: ucwords ---"); emitter.label_global("__rt_ucwords"); - emitter.instruction("sub sp, sp, #16"); // allocate stack frame - emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address - emitter.instruction("mov x29, sp"); // set frame pointer + emitter.instruction("sub sp, sp, #32"); // allocate a frame with room for the borrowed separator set + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // set frame pointer + emitter.instruction("stp x3, x4, [sp]"); // preserve the separator set across the copy, which clobbers x6-x12 emitter.instruction("bl __rt_strcopy"); // copy string to mutable concat_buf + emitter.instruction("ldp x3, x4, [sp]"); // restore the separator pointer and length after the copy emitter.instruction("cbz x2, __rt_ucwords_done"); // empty string → nothing to do emitter.instruction("mov x9, x1"); // cursor pointer emitter.instruction("mov x10, x2"); // remaining length @@ -45,14 +54,21 @@ pub fn emit_ucwords(emitter: &mut Emitter) { emitter.label("__rt_ucwords_loop"); emitter.instruction("cbz x10, __rt_ucwords_done"); // no bytes left → done emitter.instruction("ldrb w12, [x9]"); // load current byte - // -- check if current char is whitespace -- - emitter.instruction("cmp w12, #32"); // space? - emitter.instruction("b.eq __rt_ucwords_ws"); // yes → mark next as word start - emitter.instruction("cmp w12, #9"); // tab? - emitter.instruction("b.eq __rt_ucwords_ws"); // yes → mark next as word start - emitter.instruction("cmp w12, #10"); // newline? - emitter.instruction("b.eq __rt_ucwords_ws"); // yes → mark next as word start - // -- not whitespace: uppercase if word_start -- + + // -- membership test against the separator byte set -- + emitter.instruction("mov x13, x3"); // restart the separator cursor for this subject byte + emitter.instruction("mov x14, x4"); // restart the separator counter for this subject byte + emitter.label("__rt_ucwords_sep_loop"); + emitter.instruction("cbz x14, __rt_ucwords_word"); // the byte is not a separator, so it may begin or continue a word + emitter.instruction("ldrb w15, [x13], #1"); // load the next separator byte and advance the cursor + emitter.instruction("sub x14, x14, #1"); // record that one separator byte has been compared + emitter.instruction("cmp w15, w12"); // does the subject byte appear in the separator set? + emitter.instruction("b.ne __rt_ucwords_sep_loop"); // keep scanning the remaining separator bytes + emitter.instruction("mov x11, #1"); // set word_start flag for the byte after this separator + emitter.instruction("b __rt_ucwords_next"); // advance past the separator byte + + // -- not a separator: uppercase if word_start -- + emitter.label("__rt_ucwords_word"); emitter.instruction("cbz x11, __rt_ucwords_next"); // not word start → skip uppercasing emitter.instruction("cmp w12, #97"); // check if char >= 'a' emitter.instruction("b.lt __rt_ucwords_clear"); // not lowercase → just clear flag @@ -62,10 +78,6 @@ pub fn emit_ucwords(emitter: &mut Emitter) { emitter.instruction("strb w12, [x9]"); // store uppercased byte emitter.label("__rt_ucwords_clear"); emitter.instruction("mov x11, #0"); // clear word_start flag - emitter.instruction("b __rt_ucwords_next"); // advance to next char - - emitter.label("__rt_ucwords_ws"); - emitter.instruction("mov x11, #1"); // set word_start flag for next char emitter.label("__rt_ucwords_next"); emitter.instruction("add x9, x9, #1"); // advance cursor @@ -73,46 +85,71 @@ pub fn emit_ucwords(emitter: &mut Emitter) { emitter.instruction("b __rt_ucwords_loop"); // process next byte emitter.label("__rt_ucwords_done"); - emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #16"); // deallocate stack frame + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // deallocate stack frame emitter.instruction("ret"); // return with x1/x2 from strcopy } /// Emits the x86_64 Linux variant of the `__rt_ucwords` runtime helper. /// -/// Uppercases the first character of each word in a PHP byte-string. -/// Whitespace characters (space ASCII 32, tab ASCII 9, newline ASCII 10) are word separators. +/// Uppercases the first character of each word in a PHP byte-string, where a word starts at +/// the subject's first byte and after every byte that appears in the separator set. /// /// Input registers (x86_64 System V ABI): /// - rdi: pointer to the input string /// - rsi: length of the input string +/// - rdx: pointer to the separator byte set +/// - rcx: length of the separator byte set /// Output registers: -/// - rax: pointer to the result (heap-allocated via `__rt_strcopy`, refcounted) +/// - rax: pointer to the result (concat-backed mutable copy via `__rt_strcopy`) /// - rdx: length of the result string /// -/// Clobbers: r8, rcx, r9, r10. +/// The copied string's pointer and length are spilled rather than kept in registers, because +/// the separator membership scan needs both `rax` and `rdx` as scratch. +/// +/// Clobbers: rax, rcx, rdx, rdi, rsi, r8, r9, r10, r11. fn emit_ucwords_linux_x86_64(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: ucwords ---"); emitter.label_global("__rt_ucwords"); + emitter.instruction("push rbp"); // preserve the caller frame pointer across the string copy + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the spilled pointers + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the separator set and the copied string + emitter.instruction("mov QWORD PTR [rsp], rdx"); // preserve the separator pointer across the copy + emitter.instruction("mov QWORD PTR [rsp + 8], rcx"); // preserve the separator length across the copy + emitter.instruction("mov rax, rdi"); // __rt_strcopy reads its source pointer from rax, not from the SysV first argument register + emitter.instruction("mov rdx, rsi"); // __rt_strcopy reads its source length from rdx, not from the SysV second argument register emitter.instruction("call __rt_strcopy"); // copy the source string into concat storage so ucwords() can mutate bytes in place without touching borrowed input + emitter.instruction("mov QWORD PTR [rsp + 16], rax"); // spill the copied string pointer that must be returned + emitter.instruction("mov QWORD PTR [rsp + 24], rdx"); // spill the copied string length that must be returned emitter.instruction("test rdx, rdx"); // skip the word-start scan when ucwords() receives an empty string emitter.instruction("jz __rt_ucwords_done_linux_x86_64"); // return immediately when there are no bytes to uppercase emitter.instruction("mov r8, rax"); // seed the mutable string cursor with the concat-backed copy returned by __rt_strcopy emitter.instruction("mov rcx, rdx"); // seed the remaining-length counter from the copied string length returned by __rt_strcopy - emitter.instruction("mov r9, 1"); // start in word-start mode so the first non-whitespace byte can be uppercased when appropriate + emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the separator set pointer for the membership scans + emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the separator set length for the membership scans + emitter.instruction("mov r9, 1"); // start in word-start mode so the first non-separator byte can be uppercased when appropriate emitter.label("__rt_ucwords_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every byte of the concat-backed copy has been classified emitter.instruction("jz __rt_ucwords_done_linux_x86_64"); // finish once the full copied string has been processed - emitter.instruction("movzx r10d, BYTE PTR [r8]"); // load the current byte from the mutable concat-backed copy before classifying whitespace and ASCII case - emitter.instruction("cmp r10b, 32"); // is the current byte a space that marks the start of the next word? - emitter.instruction("je __rt_ucwords_ws_linux_x86_64"); // mark the next byte as a word start after a space separator - emitter.instruction("cmp r10b, 9"); // is the current byte a tab that marks the start of the next word? - emitter.instruction("je __rt_ucwords_ws_linux_x86_64"); // mark the next byte as a word start after a tab separator - emitter.instruction("cmp r10b, 10"); // is the current byte a newline that marks the start of the next word? - emitter.instruction("je __rt_ucwords_ws_linux_x86_64"); // mark the next byte as a word start after a newline separator - emitter.instruction("test r9, r9"); // should ucwords() try to uppercase the current non-whitespace byte? + emitter.instruction("movzx r10d, BYTE PTR [r8]"); // load the current byte from the mutable concat-backed copy before classifying separators and ASCII case + + emitter.instruction("mov rsi, r11"); // restart the separator cursor for this subject byte + emitter.instruction("mov rax, rdi"); // restart the separator counter for this subject byte + emitter.label("__rt_ucwords_sep_loop_linux_x86_64"); + emitter.instruction("test rax, rax"); // has the whole separator set been compared without a match? + emitter.instruction("jz __rt_ucwords_word_linux_x86_64"); // the byte is not a separator, so it may begin or continue a word + emitter.instruction("movzx edx, BYTE PTR [rsi]"); // load the next separator byte for the membership comparison + emitter.instruction("add rsi, 1"); // advance the separator cursor past the compared byte + emitter.instruction("sub rax, 1"); // record that one separator byte has been compared + emitter.instruction("cmp dl, r10b"); // does the subject byte appear in the separator set? + emitter.instruction("jne __rt_ucwords_sep_loop_linux_x86_64"); // keep scanning the remaining separator bytes + emitter.instruction("mov r9, 1"); // mark the next byte as the start of a new word after a separator + emitter.instruction("jmp __rt_ucwords_next_linux_x86_64"); // advance past the separator byte + + emitter.label("__rt_ucwords_word_linux_x86_64"); + emitter.instruction("test r9, r9"); // should ucwords() try to uppercase the current non-separator byte? emitter.instruction("jz __rt_ucwords_next_linux_x86_64"); // skip the ASCII-case conversion when the current byte is inside an existing word emitter.instruction("cmp r10b, 97"); // compare the current byte against 'a' to detect lowercase ASCII letters emitter.instruction("jb __rt_ucwords_clear_linux_x86_64"); // clear word-start mode without mutating bytes below 'a' @@ -123,10 +160,6 @@ fn emit_ucwords_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_ucwords_clear_linux_x86_64"); emitter.instruction("mov r9, 0"); // clear word-start mode after the first byte of the current word has been handled - emitter.instruction("jmp __rt_ucwords_next_linux_x86_64"); // advance to the next byte after handling the current word-start candidate - - emitter.label("__rt_ucwords_ws_linux_x86_64"); - emitter.instruction("mov r9, 1"); // mark the next non-whitespace byte as the start of a new word after a separator emitter.label("__rt_ucwords_next_linux_x86_64"); emitter.instruction("add r8, 1"); // advance the mutable string cursor after classifying the current byte @@ -134,5 +167,9 @@ fn emit_ucwords_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_ucwords_loop_linux_x86_64"); // continue processing bytes until the full copied string has been classified emitter.label("__rt_ucwords_done_linux_x86_64"); + emitter.instruction("mov rax, QWORD PTR [rsp + 16]"); // reload the copied string pointer into the standard x86_64 string result register + emitter.instruction("mov rdx, QWORD PTR [rsp + 24]"); // reload the copied string length into the standard x86_64 string length register + emitter.instruction("add rsp, 32"); // release the ucwords spill slots before returning the mutated copy + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the mutated copy emitter.instruction("ret"); // return the mutated concat-backed copy in the standard x86_64 string result registers } diff --git a/src/codegen_support/runtime/strings/urldecode.rs b/src/codegen_support/runtime/strings/urldecode.rs index a1ac6bc40d..6d9760d526 100644 --- a/src/codegen_support/runtime/strings/urldecode.rs +++ b/src/codegen_support/runtime/strings/urldecode.rs @@ -7,14 +7,18 @@ //! //! Key details: //! - URL encoding helpers are emitted byte scanners that must preserve PHP escaping rules for supported encodings. +//! - Decoding never grows the payload, so the source length is reserved through +//! `__rt_concat_reserve` before the first store; inputs beyond the 64 KiB concat scratch +//! buffer fall back to heap storage instead of running off the end of it. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// Decodes URL-encoded byte sequences in a PHP byte-string. /// Input: x1=source pointer, x2=source length (ARM64). Output: x1=result pointer, x2=result length. -/// Writes the decoded string into `_concat_buf` and advances `_concat_off`. +/// Reserves the (never-exceeded) source length through `__rt_concat_reserve` — concat scratch +/// while it fits, owned heap storage otherwise — and finishes through `__rt_concat_publish`. +/// Clobbers every caller-saved register, because the reservation can reach `__rt_heap_alloc`. /// Dispatches to x86_64 or ARM64 implementation based on `emitter.target`. pub fn emit_urldecode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -26,12 +30,16 @@ pub fn emit_urldecode(emitter: &mut Emitter) { emitter.comment("--- runtime: urldecode ---"); emitter.label_global("__rt_urldecode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case (unchanged-length) decoded result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the urldecode helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x0, x2"); // percent decoding never grows the payload, so the source length bounds the result + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_urldecode_loop"); @@ -99,15 +107,17 @@ pub fn emit_urldecode(emitter: &mut Emitter) { emitter.label("__rt_urldecode_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the urldecode helper frame emitter.instruction("ret"); // return } /// x86_64 Linux implementation of URL decoding. /// Input: rax=source pointer, rdx=source length. Output: rax=result pointer, rdx=result length. -/// Writes decoded bytes into `_concat_buf` at the current `_concat_off` offset, then advances `_concat_off`. +/// Reserves the (never-exceeded) source length through `__rt_concat_reserve` and publishes the +/// written length through `__rt_concat_publish`, so long inputs use owned heap storage instead +/// of running off the end of the 64 KiB concat scratch buffer. /// Handles '+' → space substitution and '%XX' hex decoding (both uppercase and lowercase hex digits). /// Incomplete '%' escapes at end of input are copied literally. fn emit_urldecode_linux_x86_64(emitter: &mut Emitter) { @@ -115,13 +125,17 @@ fn emit_urldecode_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: urldecode ---"); emitter.label_global("__rt_urldecode"); - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before decoding query-style percent-encoded bytes - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the decoded string begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed percent-encoded input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("mov rax, rdx"); // percent decoding never grows the payload, so the source length bounds the result + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the decoded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the decoded string begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed percent-encoded input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_urldecode_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied or decoded into concat storage @@ -191,11 +205,11 @@ fn emit_urldecode_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_urldecode_loop_linux_x86_64"); // continue decoding the remaining source bytes after one literal-byte copy emitter.label("__rt_urldecode_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after decoding the full input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the decoded string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after decoding the full input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the decoded string length emitter.instruction("sub rdx, r8"); // compute the decoded string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that urldecode() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced decoded-string length - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the urldecode() pass - emitter.instruction("ret"); // return the concat-backed decoded string in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the urldecode spill slots before returning the decoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the decoded string + emitter.instruction("ret"); // return the decoded string in the standard x86_64 string result registers } diff --git a/src/codegen_support/runtime/strings/urlencode.rs b/src/codegen_support/runtime/strings/urlencode.rs index c71cccbffe..13a2699370 100644 --- a/src/codegen_support/runtime/strings/urlencode.rs +++ b/src/codegen_support/runtime/strings/urlencode.rs @@ -7,13 +7,17 @@ //! //! Key details: //! - URL encoding helpers are emitted byte scanners that must preserve PHP escaping rules for supported encodings. +//! - The worst-case `3 * len` percent-encoded result is reserved through `__rt_concat_reserve` +//! before the first store, so long inputs fall back to heap storage instead of running off +//! the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; -use crate::codegen_support::abi; /// urlencode: percent-encode non-alphanumeric chars except -_. and space->+. -/// Input: x1/x2=string. Output: x1/x2=result in concat_buf. +/// Input: x1/x2=string. Output: x1/x2=result. +/// Reserves the worst-case `3 * len` expansion through `__rt_concat_reserve` (concat scratch +/// while it fits, owned heap storage otherwise) and finishes through `__rt_concat_publish`. pub fn emit_urlencode(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_urlencode_linux_x86_64(emitter); @@ -24,12 +28,19 @@ pub fn emit_urlencode(emitter: &mut Emitter) { emitter.comment("--- runtime: urlencode ---"); emitter.label_global("__rt_urlencode"); - // -- set up concat_buf destination -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x6", "_concat_off"); - emitter.instruction("ldr x8, [x6]"); // load current offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x7", "_concat_buf"); - emitter.instruction("add x9, x7, x8"); // destination pointer - emitter.instruction("mov x10, x9"); // save result start + // -- reserve the worst-case three-bytes-per-input-byte result before writing anything -- + emitter.instruction("sub sp, sp, #32"); // allocate spill space for the borrowed source string + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across the reservation call + emitter.instruction("add x29, sp, #16"); // establish the urlencode helper frame pointer + emitter.instruction("stp x1, x2, [sp]"); // save the source pointer and length across the reservation call + emitter.instruction("mov x9, #3"); // worst-case percent-encoded expansion factor + emitter.instruction("umulh x10, x2, x9"); // capture the high half of the 3 * length product + emitter.instruction("cbnz x10, __rt_urlencode_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x0, x2, x9"); // compute the worst-case percent-encoded result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the percent-encoded result + emitter.instruction("mov x9, x0"); // destination pointer + emitter.instruction("mov x10, x0"); // save result start + emitter.instruction("ldp x1, x2, [sp]"); // reload the borrowed source pointer and length emitter.instruction("mov x11, x2"); // remaining byte count emitter.label("__rt_urlencode_loop"); @@ -104,10 +115,14 @@ pub fn emit_urlencode(emitter: &mut Emitter) { emitter.label("__rt_urlencode_done"); emitter.instruction("mov x1, x10"); // result pointer emitter.instruction("sub x2, x9, x10"); // result length - emitter.instruction("ldr x8, [x6]"); // reload offset - emitter.instruction("add x8, x8, x2"); // advance by result length - emitter.instruction("str x8, [x6]"); // store updated offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the urlencode helper frame emitter.instruction("ret"); // return + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_urlencode_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits x86_64 Linux–specific urlencode runtime helper. @@ -118,13 +133,18 @@ fn emit_urlencode_linux_x86_64(emitter: &mut Emitter) { emitter.comment("--- runtime: urlencode ---"); emitter.label_global("__rt_urlencode"); - crate::codegen_support::abi::emit_symbol_address(emitter, "r8", "_concat_off"); - emitter.instruction("mov r9, QWORD PTR [r8]"); // load the current concat-buffer write offset before percent-encoding the borrowed source string - crate::codegen_support::abi::emit_symbol_address(emitter, "r10", "_concat_buf"); - emitter.instruction("lea r11, [r10 + r9]"); // compute the concat-buffer destination pointer where the urlencoded string begins - emitter.instruction("mov r8, r11"); // preserve the concat-backed result start pointer for the returned string value after the loop mutates the destination cursor - emitter.instruction("mov rcx, rdx"); // seed the remaining source length counter from the borrowed input string length - emitter.instruction("mov rsi, rax"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers + emitter.instruction("push rbp"); // preserve the caller frame pointer across the reservation and publish calls + emitter.instruction("mov rbp, rsp"); // establish a stable frame base for the borrowed source string + emitter.instruction("sub rsp, 32"); // reserve aligned spill slots for the source pointer and length + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the borrowed source pointer across the reservation call + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the borrowed source length across the reservation call + emitter.instruction("imul rax, rdx, 3"); // compute the worst-case percent-encoded result size as 3 * source length + emitter.instruction("jo __rt_urlencode_size_overflow_linux_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the percent-encoded result + emitter.instruction("mov r11, rax"); // compute the destination pointer where the urlencoded string begins + emitter.instruction("mov r8, r11"); // preserve the result start pointer for the returned string value after the loop mutates the destination cursor + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // seed the remaining source length counter from the borrowed input string length + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // preserve the borrowed source string cursor in a dedicated register before the loop mutates caller-saved registers emitter.label("__rt_urlencode_loop_linux_x86_64"); emitter.instruction("test rcx, rcx"); // stop once every source byte has been classified and copied or percent-encoded into concat storage @@ -196,11 +216,15 @@ fn emit_urlencode_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_urlencode_loop_linux_x86_64"); // continue encoding the remaining source bytes after copying one safe byte emitter.label("__rt_urlencode_done_linux_x86_64"); - emitter.instruction("mov rax, r8"); // return the concat-backed result start pointer after percent-encoding the full input string - emitter.instruction("mov rdx, r11"); // copy the final concat-buffer destination cursor before computing the encoded string length + emitter.instruction("mov rax, r8"); // return the reserved result start pointer after percent-encoding the full input string + emitter.instruction("mov rdx, r11"); // copy the final destination cursor before computing the encoded string length emitter.instruction("sub rdx, r8"); // compute the encoded string length as dest_end - dest_start for the returned x86_64 string value - abi::emit_load_symbol_to_reg(emitter, "rcx", "_concat_off", 0); // reload the concat-buffer write offset before publishing the bytes that urlencode() appended - emitter.instruction("add rcx, rdx"); // advance the concat-buffer write offset by the produced encoded-string length - abi::emit_store_reg_to_symbol(emitter, "rcx", "_concat_off", 0); // persist the updated concat-buffer write offset after finishing the urlencode() pass - emitter.instruction("ret"); // return the concat-backed urlencoded string in the standard x86_64 string result registers + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results + emitter.instruction("add rsp, 32"); // release the urlencode spill slots before returning the encoded string + emitter.instruction("pop rbp"); // restore the caller frame pointer before returning the encoded string + emitter.instruction("ret"); // return the urlencoded string in the standard x86_64 string result registers + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_urlencode_size_overflow_linux_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/strings/wordwrap.rs b/src/codegen_support/runtime/strings/wordwrap.rs index aa799d8051..d0c1a33005 100644 --- a/src/codegen_support/runtime/strings/wordwrap.rs +++ b/src/codegen_support/runtime/strings/wordwrap.rs @@ -8,8 +8,10 @@ //! Key details: //! - Implements PHP's algorithm: lines break at the last space at/after the wrap width; an //! over-long word is left intact unless `cut_long_words` is set, in which case it is broken at -//! the width. Existing `\n` bytes reset the current line length. Output is appended to the -//! `_concat_buf` / `_concat_off` globals as a heap-backed, refcount-compatible PHP string. +//! the width. Existing `\n` bytes reset the current line length. +//! - The worst-case `textlen * (1 + break_len)` result is reserved through +//! `__rt_concat_reserve` before the first store, so long inputs fall back to heap storage +//! instead of running off the end of the 64 KiB concat scratch buffer. use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -23,8 +25,10 @@ use crate::codegen_support::platform::Arch; /// Output registers (ARM64): x1=result ptr, x2=result len. /// Output registers (x86_64): rax=result ptr, rdx=result len. /// -/// Uses globals `_concat_buf` / `_concat_off` for output; the result is a heap-backed -/// refcount-compatible PHP string written into the concat buffer. +/// Reserves the worst-case `textlen * (1 + break_len)` result through `__rt_concat_reserve` +/// (concat scratch while it fits, owned heap storage otherwise) and finishes through +/// `__rt_concat_publish`. Clobbers every caller-saved register, because the reservation can +/// reach `__rt_heap_alloc`; a wrapped size bound reports PHP's allocation-overflow fatal. pub fn emit_wordwrap(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_wordwrap_linux_x86_64(emitter); @@ -57,11 +61,13 @@ pub fn emit_wordwrap(emitter: &mut Emitter) { emitter.instruction("mov x26, #-1"); // x26 = lastspace index (-1 = no space on line yet) emitter.instruction("mov x27, #0"); // x27 = current scan index - // -- compute output destination in the concat buffer -- - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load the current concat-buffer write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "x11", "_concat_buf"); - emitter.instruction("add x28, x11, x10"); // x28 = output write pointer = buf + offset + // -- reserve the worst-case wrapped result before writing anything -- + emitter.instruction("add x9, x23, #1"); // worst case is one break string inserted after every source byte + emitter.instruction("umulh x10, x20, x9"); // capture the high half of the textlen * (1 + break length) product + emitter.instruction("cbnz x10, __rt_wordwrap_size_overflow"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("mul x0, x20, x9"); // compute the worst-case wrapped result size + emitter.instruction("bl __rt_concat_reserve"); // reserve scratch or heap storage for the wrapped result + emitter.instruction("mov x28, x0"); // x28 = output write pointer at the reserved payload start emitter.instruction("str x28, [sp, #0]"); // save the result start pointer for the final length // -- main scan loop -- @@ -144,13 +150,10 @@ pub fn emit_wordwrap(emitter: &mut Emitter) { emitter.instruction("add x9, x19, x25"); // source = base + laststart emitter.instruction("bl __rt_wordwrap_cpy"); // copy the trailing line to output - // -- finalize result pointer/length and publish the new concat offset -- + // -- finalize result pointer/length and publish the written bytes -- emitter.instruction("ldr x1, [sp, #0]"); // x1 = result start pointer emitter.instruction("sub x2, x28, x1"); // x2 = result length = end - start - crate::codegen_support::abi::emit_symbol_address(emitter, "x9", "_concat_off"); - emitter.instruction("ldr x10, [x9]"); // load the current concat offset - emitter.instruction("add x10, x10, x2"); // advance it by the wrapped length - emitter.instruction("str x10, [x9]"); // publish the updated concat offset + emitter.instruction("bl __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results // -- restore callee-saved registers and return -- emitter.instruction("ldp x19, x20, [sp, #16]"); // restore x19, x20 @@ -173,6 +176,10 @@ pub fn emit_wordwrap(emitter: &mut Emitter) { emitter.instruction("b.ne __rt_wordwrap_cpy_loop"); // continue until all bytes are copied emitter.label("__rt_wordwrap_cpy_ret"); emitter.instruction("ret"); // return to the wrapping loop + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_wordwrap_size_overflow"); + emitter.instruction("b __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline cross-atom safe } /// Emits the x86_64 Linux implementation of the word-aware `__rt_wordwrap` runtime helper. @@ -211,11 +218,13 @@ fn emit_wordwrap_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("xor r13, r13"); // r13 = laststart = 0 emitter.instruction("mov r14, -1"); // r14 = lastspace = -1 (no space on line yet) - // -- compute the output destination in the concat buffer -- - crate::codegen_support::abi::emit_symbol_address(emitter, "rsi", "_concat_off"); - emitter.instruction("mov r10, QWORD PTR [rsi]"); // load the current concat-buffer write offset - crate::codegen_support::abi::emit_symbol_address(emitter, "r11", "_concat_buf"); - emitter.instruction("lea r15, [r11 + r10]"); // r15 = output write pointer = buf + offset + // -- reserve the worst-case wrapped result before writing anything -- + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the break-string length before deriving the worst-case expansion factor + emitter.instruction("add rax, 1"); // worst case is one break string inserted after every source byte + emitter.instruction("imul rax, QWORD PTR [rbp - 56]"); // compute the worst-case wrapped result size as textlen * (1 + break length) + emitter.instruction("jo __rt_wordwrap_size_overflow_x86_64"); // reject a wrapped size instead of reserving a too-small destination + emitter.instruction("call __rt_concat_reserve"); // reserve scratch or heap storage for the wrapped result + emitter.instruction("mov r15, rax"); // r15 = output write pointer at the reserved payload start emitter.instruction("mov QWORD PTR [rbp - 64], r15"); // save the result start pointer for the final length // -- main scan loop -- @@ -306,14 +315,11 @@ fn emit_wordwrap_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("lea rsi, [rbx + r13]"); // source = base + laststart emitter.instruction("call __rt_wordwrap_cpy_x86_64"); // copy the trailing line to output - // -- finalize result pointer/length and publish the new concat offset -- + // -- finalize result pointer/length and publish the written bytes -- emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // rax = result start pointer emitter.instruction("mov rdx, r15"); // rdx = output end pointer emitter.instruction("sub rdx, rax"); // rdx = result length = end - start - crate::codegen_support::abi::emit_symbol_address(emitter, "rsi", "_concat_off"); - emitter.instruction("mov r10, QWORD PTR [rsi]"); // load the current concat offset - emitter.instruction("add r10, rdx"); // advance it by the wrapped length - emitter.instruction("mov QWORD PTR [rsi], r10"); // publish the updated concat offset + emitter.instruction("call __rt_concat_publish"); // advance the concat scratch offset only for scratch-backed results // -- restore callee-saved registers and return -- emitter.instruction("add rsp, 64"); // release the spill slots @@ -339,4 +345,8 @@ fn emit_wordwrap_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jnz __rt_wordwrap_cpy_loop_x86_64"); // continue until all bytes are copied emitter.label("__rt_wordwrap_cpy_ret_x86_64"); emitter.instruction("ret"); // return to the wrapping loop + + // -- impossible result size: report the shared allocation-overflow fatal error -- + emitter.label("__rt_wordwrap_size_overflow_x86_64"); + emitter.instruction("jmp __rt_alloc_overflow"); // unconditional branch keeps the fatal trampoline reachable from every caller } diff --git a/src/codegen_support/runtime/system/json_ftoa.rs b/src/codegen_support/runtime/system/json_ftoa.rs index dae828f7da..e488ac2dbb 100644 --- a/src/codegen_support/runtime/system/json_ftoa.rs +++ b/src/codegen_support/runtime/system/json_ftoa.rs @@ -12,7 +12,8 @@ //! - `crate::codegen_support::runtime::emitters::emit_runtime()` via //! `crate::codegen_support::runtime::system`. //! - `__rt_json_encode_float` (same module group) passes `'e'` for the finite -//! and substituted-zero paths; `__rt_serialize` passes `'E'`. +//! and substituted-zero paths; `__rt_serialize` and `__rt_ftoa_repr` (the +//! `var_dump` float renderer) pass `'E'`. //! //! Key details: //! - The shortest precision is found by probing `snprintf("%.*e", p, x)` for @@ -23,8 +24,12 @@ //! - The decimal exponent `E` is parsed from the `%e` scratch via `strtol`; //! `decpt = E + 1` selects exponential layout when `decpt < -3 || decpt > 17` //! (the same thresholds PHP/`zend_gcvt` use), otherwise a decimal layout is -//! produced with `snprintf("%.*f", max(0, p - E), x)` straight into -//! `_concat_buf`. +//! produced with `snprintf("%.*f", p - E, x)` straight into `_concat_buf`. +//! - When `p - E` is NEGATIVE (`decpt` past the last significant digit) that +//! `snprintf` is wrong: `%.0f` prints the double's exact decimal expansion +//! (`39528480211503568`) instead of `zend_gcvt`'s shortest round-trip digits +//! zero-padded to `decpt` (`39528480211503570`). That range is emitted by hand +//! instead, copying the scratch digits and appending `E - p` zeros. //! - Output ABI matches `__rt_ftoa`: result bytes land in `_concat_buf` at the //! current `_concat_off`, the cursor is advanced by the byte count, and the //! pointer/length are returned in `x1`/`x2` (AArch64) or `rax`/`rdx` @@ -110,10 +115,10 @@ pub(crate) fn emit_json_ftoa(emitter: &mut Emitter) { emitter.instruction("cmp x9, #17"); // compare decpt against 17 emitter.instruction("b.gt __rt_json_ftoa_exp"); // decpt > 17 -> exponential form - // -- decimal form: snprintf("%.*f", max(0, p - E), x) into concat_buf -- + // -- decimal form: snprintf("%.*f", p - E, x) into concat_buf -- emitter.instruction("sub x9, x19, x21"); // fracdigits = p - E emitter.instruction("cmp x9, #0"); // is the fractional digit count negative? - emitter.instruction("csel x9, x9, xzr, ge"); // clamp negatives to zero (integer-valued) + emitter.instruction("b.lt __rt_json_ftoa_intpad"); // decpt exceeds the significant digits: zero-pad instead emitter.instruction("str x9, [sp, #0]"); // Apple variadic arg 0: fractional digit count (stack) abi::emit_symbol_address(emitter, "x10", "_concat_off"); emitter.instruction("ldr x11, [x10]"); // current concat offset @@ -134,6 +139,50 @@ pub(crate) fn emit_json_ftoa(emitter: &mut Emitter) { emitter.instruction("str x10, [x9]"); // publish the new concat offset emitter.instruction("b __rt_json_ftoa_done"); // finished decimal layout + // -- zero-padded integer form: the shortest digits followed by (E - p) zeros -- + // `%.*f` cannot be used here: with a clamped precision of 0 it prints the double's + // EXACT decimal expansion (39528480211503568) instead of `zend_gcvt`'s shortest + // round-trip digits padded with zeros (39528480211503570). + emitter.label("__rt_json_ftoa_intpad"); + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // current concat offset + abi::emit_symbol_address(emitter, "x11", "_concat_buf"); + emitter.instruction("add x12, x11, x10"); // cursor = concat_buf + offset + emitter.instruction("mov x1, x12"); // remember the result start pointer + emitter.instruction("cbz x20, __rt_json_ftoa_intpad_first"); // skip sign when the value is non-negative + emitter.instruction("mov w13, #45"); // ASCII '-' + emitter.instruction("strb w13, [x12], #1"); // emit the sign and advance the cursor + emitter.label("__rt_json_ftoa_intpad_first"); + emitter.instruction("add x13, sp, #16"); // base of the "%.*e" scratch string + emitter.instruction("ldrb w14, [x13, x20]"); // first significant digit (after optional sign) + emitter.instruction("strb w14, [x12], #1"); // emit the leading digit + emitter.instruction("cbz x19, __rt_json_ftoa_intpad_zeros"); // p==0 has no fractional digits to copy + emitter.instruction("add x13, x13, x20"); // skip optional sign + emitter.instruction("add x13, x13, #2"); // skip the leading digit and '.' + emitter.instruction("mov x14, #0"); // significant-digit copy index + emitter.label("__rt_json_ftoa_intpad_loop"); + emitter.instruction("cmp x14, x19"); // copied all p remaining significant digits? + emitter.instruction("b.ge __rt_json_ftoa_intpad_zeros"); // significant digits complete + emitter.instruction("ldrb w15, [x13, x14]"); // load the next significant digit + emitter.instruction("strb w15, [x12], #1"); // emit the significant digit + emitter.instruction("add x14, x14, #1"); // advance the copy index + emitter.instruction("b __rt_json_ftoa_intpad_loop"); // copy the next significant digit + emitter.label("__rt_json_ftoa_intpad_zeros"); + emitter.instruction("sub x14, x21, x19"); // trailing zero count = E - p + emitter.label("__rt_json_ftoa_intpad_zloop"); + emitter.instruction("cbz x14, __rt_json_ftoa_intpad_end"); // all trailing zeros emitted + emitter.instruction("mov w15, #48"); // ASCII '0' + emitter.instruction("strb w15, [x12], #1"); // emit one trailing zero + emitter.instruction("sub x14, x14, #1"); // one fewer trailing zero to emit + emitter.instruction("b __rt_json_ftoa_intpad_zloop"); // continue padding + emitter.label("__rt_json_ftoa_intpad_end"); + emitter.instruction("sub x2, x12, x1"); // result length = cursor - start + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // original concat offset + emitter.instruction("add x10, x10, x2"); // advance past the emitted bytes + emitter.instruction("str x10, [x9]"); // publish the new concat offset + emitter.instruction("b __rt_json_ftoa_done"); // finished zero-padded integer layout + // -- exponential form: d.dddde[+-]E with json conventions, byte by byte -- emitter.label("__rt_json_ftoa_exp"); abi::emit_symbol_address(emitter, "x9", "_concat_off"); @@ -283,9 +332,7 @@ fn emit_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rcx, rbx"); // fracdigits = p ... emitter.instruction("sub rcx, r13"); // ... minus E emitter.instruction("test rcx, rcx"); // is the fractional digit count negative? - emitter.instruction("jns __rt_json_ftoa_frac_ok_x"); // non-negative count is fine - emitter.instruction("xor ecx, ecx"); // clamp to zero (integer-valued) - emitter.label("__rt_json_ftoa_frac_ok_x"); + emitter.instruction("js __rt_json_ftoa_intpad_x"); // decpt exceeds the significant digits: zero-pad instead abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // current concat offset abi::emit_symbol_address(emitter, "r9", "_concat_buf"); emitter.instruction("lea rdi, [r9 + r8]"); // destination = concat_buf + offset @@ -302,6 +349,51 @@ fn emit_x86_64(emitter: &mut Emitter) { abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset emitter.instruction("jmp __rt_json_ftoa_done_x"); // finished decimal layout + // -- zero-padded integer form: the shortest digits followed by (E - p) zeros -- + emitter.label("__rt_json_ftoa_intpad_x"); + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // current concat offset + abi::emit_symbol_address(emitter, "r9", "_concat_buf"); + emitter.instruction("lea r10, [r9 + r8]"); // cursor = concat_buf + offset + emitter.instruction("mov r14, r10"); // remember the result start pointer + emitter.instruction("test r12, r12"); // is the value negative? + emitter.instruction("jz __rt_json_ftoa_intpad_first_x"); // skip sign when non-negative + emitter.instruction("mov BYTE PTR [r10], 45"); // emit '-' + emitter.instruction("inc r10"); // advance the cursor + emitter.label("__rt_json_ftoa_intpad_first_x"); + emitter.instruction("movzx ecx, BYTE PTR [rsp + r12]"); // first significant digit (after optional sign) + emitter.instruction("mov BYTE PTR [r10], cl"); // emit the leading digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("test rbx, rbx"); // does the mantissa have further digits? + emitter.instruction("jz __rt_json_ftoa_intpad_zeros_x"); // p==0 has no fractional digits to copy + emitter.instruction("lea rsi, [rsp + r12 + 2]"); // &scratch[neg+2] = first fractional digit + emitter.instruction("xor edi, edi"); // significant-digit copy index + emitter.label("__rt_json_ftoa_intpad_loop_x"); + emitter.instruction("cmp rdi, rbx"); // copied all p remaining significant digits? + emitter.instruction("jge __rt_json_ftoa_intpad_zeros_x"); // significant digits complete + emitter.instruction("movzx ecx, BYTE PTR [rsi + rdi]"); // load the next significant digit + emitter.instruction("mov BYTE PTR [r10], cl"); // emit the significant digit + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("inc rdi"); // advance the copy index + emitter.instruction("jmp __rt_json_ftoa_intpad_loop_x"); // copy the next significant digit + emitter.label("__rt_json_ftoa_intpad_zeros_x"); + emitter.instruction("mov rcx, r13"); // trailing zero count = E ... + emitter.instruction("sub rcx, rbx"); // ... minus p + emitter.label("__rt_json_ftoa_intpad_zloop_x"); + emitter.instruction("test rcx, rcx"); // all trailing zeros emitted? + emitter.instruction("jz __rt_json_ftoa_intpad_end_x"); // zero padding complete + emitter.instruction("mov BYTE PTR [r10], 48"); // emit one trailing zero + emitter.instruction("inc r10"); // advance the cursor + emitter.instruction("dec rcx"); // one fewer trailing zero to emit + emitter.instruction("jmp __rt_json_ftoa_intpad_zloop_x"); // continue padding + emitter.label("__rt_json_ftoa_intpad_end_x"); + emitter.instruction("mov rax, r14"); // result pointer = start + emitter.instruction("mov rdx, r10"); // cursor (one past the last byte) + emitter.instruction("sub rdx, rax"); // result length = cursor - start + abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // original concat offset + emitter.instruction("add r8, rdx"); // advance past the emitted bytes + abi::emit_store_reg_to_symbol(emitter, "r8", "_concat_off", 0); // publish the new concat offset + emitter.instruction("jmp __rt_json_ftoa_done_x"); // finished zero-padded integer layout + emitter.label("__rt_json_ftoa_exp_x"); abi::emit_load_symbol_to_reg(emitter, "r8", "_concat_off", 0); // current concat offset abi::emit_symbol_address(emitter, "r9", "_concat_buf"); diff --git a/src/codegen_support/runtime/system/mod.rs b/src/codegen_support/runtime/system/mod.rs index ba1aa6ecf7..9d5d0126b1 100644 --- a/src/codegen_support/runtime/system/mod.rs +++ b/src/codegen_support/runtime/system/mod.rs @@ -50,6 +50,7 @@ mod preg_strip; mod regex_locale; mod serialize; mod shell_exec; +mod stack_guard; mod unserialize; mod strtotime; mod time; @@ -85,6 +86,10 @@ pub(crate) use json_encode_mixed::emit_json_encode_mixed; pub(crate) use json_pretty::emit_json_pretty_helpers; pub(crate) use json_throw_error::emit_json_throw_error; pub(crate) use match_unhandled::emit_match_unhandled; +pub(crate) use stack_guard::{ + emit_stack_limit_init, emit_stack_overflow, STACK_GUARD_RESERVE_BYTES, + STACK_LIMIT_MAIN_SYMBOL, STACK_LIMIT_SYMBOL, +}; pub(crate) use microtime::emit_microtime; pub(crate) use microtime::emit_microtime_build_into; pub(crate) use microtime::emit_microtime_str; diff --git a/src/codegen_support/runtime/system/stack_guard.rs b/src/codegen_support/runtime/system/stack_guard.rs new file mode 100644 index 0000000000..3d00d6ffb1 --- /dev/null +++ b/src/codegen_support/runtime/system/stack_guard.rs @@ -0,0 +1,273 @@ +//! Purpose: +//! Emits the runtime half of the call-stack overflow guard: `__rt_stack_limit_init` +//! measures the running stack and publishes the low-water address every compiled +//! function prologue compares against, and `__rt_stack_overflow` is the controlled +//! fatal reached when a prologue finds the stack pointer below that address. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` via `crate::codegen_support::runtime::system`. +//! +//! Key details: +//! - `_stack_limit` is the *current* execution context's floor. It is zero-initialized, +//! and a zero value disables the guard, so any program that never reaches the +//! initializer behaves exactly as it did before the guard existed. +//! - `_stack_limit_main` remembers the OS-thread floor so `__rt_fiber_switch` can restore +//! it when control leaves a coroutine stack; fiber stacks get their own floor derived +//! from the fiber's mmap base. +//! - `__rt_stack_overflow` never returns and never needs a valid frame: prologues reach it +//! with a plain branch, so it is safe to enter with almost no stack left. + +use crate::codegen_support::runtime::data::STACK_OVERFLOW_MSG; +use crate::codegen_support::{abi, emit::Emitter, platform::Arch}; + +/// Symbol holding the low-water stack address for the currently running context. +/// Zero means "guard disabled"; every prologue check is an unsigned compare against it. +pub(crate) const STACK_LIMIT_SYMBOL: &str = "_stack_limit"; + +/// Symbol holding the OS-thread (non-fiber) stack floor, used to restore +/// `_stack_limit` when a fiber switch returns to the main context. +pub(crate) const STACK_LIMIT_MAIN_SYMBOL: &str = "_stack_limit_main"; + +/// Bytes kept in reserve below the published limit. +/// +/// The prologue check runs *after* the frame has been reserved, so this margin only has to +/// cover what a single guarded frame can still consume before the next guarded call: +/// outgoing stack-argument areas, `__rt_*` helper frames, and the libc calls those make. +/// 32 KiB is far above any of those, costs 0.4% of a default 8 MiB OS stack, and stays at +/// one eighth of the 256 KiB coroutine stack a Fiber or Generator body runs on — the one +/// place where an over-generous reserve would visibly cut the usable recursion depth. +pub(crate) const STACK_GUARD_RESERVE_BYTES: i64 = 32 * 1024; + +/// `RLIMIT_STACK` resource number. Identical (3) on Linux and macOS. +const RLIMIT_STACK: i64 = 3; + +/// Upper clamp on the measured stack budget. +/// +/// `getrlimit` reports `RLIM_INFINITY` for an unlimited stack (`0xFFFF_FFFF_FFFF_FFFF` on +/// Linux, `0x7FFF_FFFF_FFFF_FFFF` on macOS). Clamping keeps the computed floor a real +/// address instead of wrapping below zero; the cost is that a genuinely unlimited stack +/// reports the fatal after 64 MiB instead of running until the OS refuses to grow it. +const STACK_BUDGET_CAP_BYTES: i64 = 64 * 1024 * 1024; + +/// Budget used when `getrlimit` fails outright (8 MiB — the default on both platforms). +const STACK_BUDGET_FALLBACK_BYTES: i64 = 8 * 1024 * 1024; + +/// Smallest budget worth guarding. Below this the reserve would swallow the whole stack, +/// so the guard is disabled instead of publishing a floor that is effectively at the +/// current stack pointer. +const STACK_BUDGET_MIN_BYTES: i64 = 256 * 1024; + +/// Emits `__rt_stack_limit_init`, which measures the running OS stack once at process +/// start and publishes the resulting floor into `_stack_limit` and `_stack_limit_main`. +/// +/// Takes no arguments and returns nothing. Clobbers the caller-saved registers a plain +/// `getrlimit(RLIMIT_STACK, &rlimit)` call clobbers, so callers must invoke it before any +/// live argument register matters (the `main` prologue calls it after argc/argv have been +/// stored to globals). +/// +/// The published floor is `entry_sp - min(rlim_cur, 64 MiB) + 64 KiB`. When `getrlimit` +/// fails, the reported limit is implausibly small, or the subtraction would wrap, zero is +/// published instead and the guard stays inert for the whole process. +pub fn emit_stack_limit_init(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: stack_limit_init (publish the call-stack floor) ---"); + emitter.label_global("__rt_stack_limit_init"); + match emitter.target.arch { + Arch::AArch64 => emit_stack_limit_init_aarch64(emitter), + Arch::X86_64 => emit_stack_limit_init_x86_64(emitter), + } +} + +/// AArch64 implementation of `__rt_stack_limit_init` (macOS and Linux share it). +/// +/// Reserves a 32-byte frame: the low 16 bytes are the `struct rlimit` output buffer and +/// the high 16 bytes hold the saved x29/x30 pair. x29 doubles as the "stack top" reference +/// because it is callee-saved and therefore survives the `getrlimit` call unchanged. +fn emit_stack_limit_init_aarch64(emitter: &mut Emitter) { + emitter.instruction("sub sp, sp, #32"); // reserve the rlimit output buffer plus this helper's frame footer + emitter.instruction("stp x29, x30, [sp, #16]"); // save the caller frame pointer and return address + emitter.instruction("add x29, sp, #16"); // anchor the frame pointer and remember it as the stack-top reference + + // -- getrlimit(RLIMIT_STACK, &rlimit) -- + emitter.instruction(&format!("mov x0, #{}", RLIMIT_STACK)); // resource = RLIMIT_STACK + emitter.instruction("mov x1, sp"); // destination = the 16-byte rlimit buffer at the bottom of this frame + emitter.bl_c("getrlimit"); // x0 = 0 on success, -1 on failure + + // -- pick the budget: rlim_cur on success, the platform default otherwise -- + abi::emit_load_int_immediate(emitter, "x1", STACK_BUDGET_FALLBACK_BYTES); + emitter.instruction("cbnz x0, __rt_stack_limit_init_clamp"); // keep the fallback budget when getrlimit reported a failure + emitter.instruction("ldr x1, [sp]"); // x1 = rlim_cur, the soft stack limit in bytes + + // -- clamp the budget into a range that yields a real address -- + emitter.label("__rt_stack_limit_init_clamp"); + abi::emit_load_int_immediate(emitter, "x2", STACK_BUDGET_CAP_BYTES); + emitter.instruction("cmp x1, x2"); // is the reported budget above the cap (or RLIM_INFINITY)? + emitter.instruction("csel x1, x1, x2, lo"); // x1 = min(budget, cap) using an unsigned comparison + abi::emit_load_int_immediate(emitter, "x2", STACK_BUDGET_MIN_BYTES); + emitter.instruction("cmp x1, x2"); // is the budget too small to be worth guarding? + emitter.instruction("b.lo __rt_stack_limit_init_disable"); // yes — leave the guard disabled rather than publish a bogus floor + + // -- floor = entry stack pointer - (budget - reserve) -- + abi::emit_load_int_immediate(emitter, "x2", STACK_GUARD_RESERVE_BYTES); + emitter.instruction("sub x1, x1, x2"); // subtract the reserve kept for helper frames and the fatal path + emitter.instruction("sub x0, x29, x1"); // x0 = the lowest stack address compiled prologues may reach + emitter.instruction("cmp x0, x29"); // did the subtraction wrap below address zero? + emitter.instruction("b.hs __rt_stack_limit_init_disable"); // yes — an unusable floor, so disable the guard instead + emitter.instruction("b __rt_stack_limit_init_store"); // publish the computed floor + + // -- disabled: publish zero so every prologue compare passes -- + emitter.label("__rt_stack_limit_init_disable"); + emitter.instruction("mov x0, #0"); // zero disables the guard for the rest of the process + + emitter.label("__rt_stack_limit_init_store"); + abi::emit_store_reg_to_symbol(emitter, "x0", STACK_LIMIT_SYMBOL, 0); + abi::emit_store_reg_to_symbol(emitter, "x0", STACK_LIMIT_MAIN_SYMBOL, 0); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore the caller frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the rlimit buffer and frame footer + emitter.instruction("ret"); // return to the process entry prologue +} + +/// x86_64 (Linux) implementation of `__rt_stack_limit_init`. +/// +/// Mirrors the AArch64 path with SysV registers. `rbp` is the stack-top reference because +/// it is callee-saved and therefore preserved across the `getrlimit` call; the 16 bytes +/// below it are the `struct rlimit` output buffer, which also keeps `rsp` 16-byte aligned +/// at the call site. +fn emit_stack_limit_init_x86_64(emitter: &mut Emitter) { + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // anchor the frame pointer and remember it as the stack-top reference + emitter.instruction("sub rsp, 16"); // reserve the 16-byte rlimit output buffer, keeping rsp 16-byte aligned + + // -- getrlimit(RLIMIT_STACK, &rlimit) -- + emitter.instruction(&format!("mov edi, {}", RLIMIT_STACK)); // resource = RLIMIT_STACK + emitter.instruction("mov rsi, rsp"); // destination = the 16-byte rlimit buffer below the frame pointer + emitter.bl_c("getrlimit"); // eax = 0 on success, -1 on failure + + // -- pick the budget: rlim_cur on success, the platform default otherwise -- + emitter.instruction(&format!("mov rcx, {}", STACK_BUDGET_FALLBACK_BYTES)); // preload the fallback budget without disturbing the result flags + emitter.instruction("test eax, eax"); // did getrlimit succeed? + emitter.instruction("jnz __rt_stack_limit_init_clamp"); // keep the fallback budget when getrlimit reported a failure + emitter.instruction("mov rcx, QWORD PTR [rsp]"); // rcx = rlim_cur, the soft stack limit in bytes + + // -- clamp the budget into a range that yields a real address -- + emitter.label("__rt_stack_limit_init_clamp"); + emitter.instruction(&format!("mov rdx, {}", STACK_BUDGET_CAP_BYTES)); // materialize the budget cap for the clamp comparison + emitter.instruction("cmp rcx, rdx"); // is the reported budget above the cap (or RLIM_INFINITY)? + emitter.instruction("cmova rcx, rdx"); // rcx = min(budget, cap) using an unsigned comparison + emitter.instruction(&format!("mov rdx, {}", STACK_BUDGET_MIN_BYTES)); // materialize the smallest budget worth guarding + emitter.instruction("cmp rcx, rdx"); // is the budget too small to be worth guarding? + emitter.instruction("jb __rt_stack_limit_init_disable"); // yes — leave the guard disabled rather than publish a bogus floor + + // -- floor = entry stack pointer - (budget - reserve) -- + emitter.instruction(&format!("sub rcx, {}", STACK_GUARD_RESERVE_BYTES)); // subtract the reserve kept for helper frames and the fatal path + emitter.instruction("mov rax, rbp"); // start from the remembered stack-top reference + emitter.instruction("sub rax, rcx"); // rax = the lowest stack address compiled prologues may reach + emitter.instruction("cmp rax, rbp"); // did the subtraction wrap below address zero? + emitter.instruction("jae __rt_stack_limit_init_disable"); // yes — an unusable floor, so disable the guard instead + emitter.instruction("jmp __rt_stack_limit_init_store"); // publish the computed floor + + // -- disabled: publish zero so every prologue compare passes -- + emitter.label("__rt_stack_limit_init_disable"); + emitter.instruction("xor eax, eax"); // zero disables the guard for the rest of the process + + emitter.label("__rt_stack_limit_init_store"); + abi::emit_store_reg_to_symbol(emitter, "rax", STACK_LIMIT_SYMBOL, 0); + abi::emit_store_reg_to_symbol(emitter, "rax", STACK_LIMIT_MAIN_SYMBOL, 0); + emitter.instruction("mov rsp, rbp"); // release the rlimit output buffer + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the process entry prologue +} + +/// Emits `__rt_stack_overflow`, the controlled fatal for call-stack exhaustion. +/// +/// Reached by a plain branch (never a call) from a function prologue that found the stack +/// pointer below `_stack_limit`, so it must not assume a usable frame and never returns. +/// Writes PHP's stack-overflow wording to stderr and exits with status 255, the status PHP +/// uses for an uncaught fatal error. +pub fn emit_stack_overflow(emitter: &mut Emitter) { + let msg_len = STACK_OVERFLOW_MSG.len(); + emitter.blank(); + emitter.comment("--- runtime: stack_overflow (controlled call-depth fatal) ---"); + emitter.label_global("__rt_stack_overflow"); + match emitter.target.arch { + Arch::AArch64 => { + abi::emit_symbol_address(emitter, "x1", "_stack_err_msg"); + emitter.instruction(&format!("mov x2, #{}", msg_len)); // byte length of the call-stack overflow message + emitter.instruction("mov x0, #2"); // write the diagnostic to stderr + emitter.syscall(4); + emitter.instruction("mov x0, #255"); // exit status 255, matching PHP's uncaught fatal error + emitter.syscall(1); + } + Arch::X86_64 => { + abi::emit_symbol_address(emitter, "rsi", "_stack_err_msg"); + emitter.instruction(&format!("mov edx, {}", msg_len)); // byte length of the call-stack overflow message + emitter.instruction("mov edi, 2"); // write the diagnostic to stderr + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall number 1 = write + emitter.instruction("syscall"); // emit the call-stack overflow diagnostic + emitter.instruction("mov edi, 255"); // exit status 255, matching PHP's uncaught fatal error + emitter.instruction("mov eax, 60"); // Linux x86_64 syscall number 60 = exit + emitter.instruction("syscall"); // terminate the process after reporting the overflow + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::codegen_support::platform::{Platform, Target}; + + /// Every supported target must emit both guard helpers, and the initializer must reach + /// `getrlimit` through the platform's C-symbol spelling rather than a hardcoded name. + #[test] + fn test_stack_guard_helpers_emit_for_every_supported_target() { + for (target, getrlimit_call) in [ + (Target::new(Platform::MacOS, Arch::AArch64), "bl _getrlimit"), + (Target::new(Platform::Linux, Arch::AArch64), "bl getrlimit"), + (Target::new(Platform::Linux, Arch::X86_64), "call getrlimit"), + ] { + let mut emitter = Emitter::new(target); + emit_stack_limit_init(&mut emitter); + emit_stack_overflow(&mut emitter); + let asm = emitter.output(); + assert!(asm.contains("__rt_stack_limit_init:"), "{target:?}: {asm}"); + assert!(asm.contains("__rt_stack_overflow:"), "{target:?}: {asm}"); + assert!(asm.contains(getrlimit_call), "{target:?}: {asm}"); + assert!(asm.contains("_stack_limit"), "{target:?}: {asm}"); + assert!(asm.contains("_stack_limit_main"), "{target:?}: {asm}"); + assert!(asm.contains("_stack_err_msg"), "{target:?}: {asm}"); + } + } + + /// The fatal must exit with 255, the status PHP reports for an uncaught fatal error, + /// and must write exactly as many bytes as the message actually has. + #[test] + fn test_stack_overflow_reports_php_exit_status_and_exact_length() { + let len = STACK_OVERFLOW_MSG.len(); + let mut arm = Emitter::new(Target::new(Platform::MacOS, Arch::AArch64)); + emit_stack_overflow(&mut arm); + let arm = arm.output(); + assert!(arm.contains(&format!("mov x2, #{len}")), "{arm}"); + assert!(arm.contains("mov x0, #255"), "{arm}"); + + let mut x86 = Emitter::new(Target::new(Platform::Linux, Arch::X86_64)); + emit_stack_overflow(&mut x86); + let x86 = x86.output(); + assert!(x86.contains(&format!("mov edx, {len}")), "{x86}"); + assert!(x86.contains("mov edi, 255"), "{x86}"); + } + + /// The reserve has to stay well below the default fiber stack, or the floor published + /// on a coroutine stack would sit above its initial stack pointer and every generator + /// body would trip the guard on its first frame. + #[test] + fn test_reserve_leaves_usable_room_on_a_default_fiber_stack() { + let fiber_usable = + i64::from(crate::codegen_support::runtime::fibers::FIBER_DEFAULT_STACK_SIZE); + assert!( + STACK_GUARD_RESERVE_BYTES * 8 <= fiber_usable, + "reserve {STACK_GUARD_RESERVE_BYTES} is too large for a {fiber_usable}-byte fiber stack" + ); + assert!(STACK_BUDGET_MIN_BYTES > STACK_GUARD_RESERVE_BYTES); + assert!(STACK_BUDGET_FALLBACK_BYTES <= STACK_BUDGET_CAP_BYTES); + } +} diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index 199c50816e..1bc4825e3f 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -22,7 +22,7 @@ use super::exprs::rewrite_expr; pub(super) fn apply_stmts(stmts: Vec, defines: &HashSet) -> Vec { let mut result = Vec::new(); for stmt in stmts { - let _source_mode = crate::source::scoped_parse_mode(stmt.source_mode); + let _source_mode = crate::source::scoped_parse_mode(stmt.profile()); match stmt.kind { StmtKind::IfDef { symbol, diff --git a/src/debug_info.rs b/src/debug_info.rs index cba4079dcb..112b2ccbaf 100644 --- a/src/debug_info.rs +++ b/src/debug_info.rs @@ -19,10 +19,17 @@ //! silently dropped). All of it is hand-encoded here, the same way //! `-g`-enabled assemblers do it. //! - Markers only appear in the text section, which keeps `.loc` legal. +//! - Every value spliced into assembly text is attacker-influenced (the source +//! path and the working directory come from the invocation). Quoted operands +//! go through `escape_asm_string()`, and the one unquoted operand (a +//! subprogram entry symbol) is validated by `is_plain_asm_symbol()`; an +//! unescaped `\` or `"` in a path would otherwise terminate the directive +//! string early and let the rest of the path be assembled as directives. use std::fmt::Write as _; use crate::codegen::platform::Platform; +use crate::codegen_support::runtime::data::instanceof::escaped_bytes; /// One `@fn`..`@endfn` region collected during injection, used to emit its /// `DW_TAG_subprogram`: the PHP-level name, the entry symbol (`DW_AT_low_pc`), @@ -40,7 +47,7 @@ pub fn inject_line_directives(asm: &str, source_path: &str, platform: Platform) let mut out = String::with_capacity(asm.len() + asm.len() / 4); out.push_str(&format!( ".file 1 \"{}\"\n", - source_path.replace('"', "\\\"") + escape_asm_string(source_path) )); let mut subprograms: Vec = Vec::new(); @@ -60,7 +67,9 @@ pub fn inject_line_directives(asm: &str, source_path: &str, platform: Platform) if let (Some(name), Some(symbol)) = (marker_value(marker, "name"), marker_value(marker, "symbol")) { - open = Some((name.to_string(), symbol.to_string())); + if is_plain_asm_symbol(symbol) { + open = Some((name.to_string(), symbol.to_string())); + } } continue; } @@ -164,9 +173,36 @@ Lelephc_debug_cu_start: out } -/// Escapes a value for use inside a double-quoted assembler string. +/// Escapes a value for use inside a double-quoted assembler string literal. +/// +/// Delegates to the shared `escaped_bytes()` encoder that already backs the +/// runtime `.ascii` data section, so `.file` and every `.asciz` emitted here +/// obey one escaping contract: `\` and `"` are backslash-escaped, newline and +/// tab use their short forms, and every other byte outside printable ASCII +/// (carriage return, NUL, UTF-8 continuation bytes) becomes a 3-digit octal +/// escape. Both GNU `as` and the LLVM integrated assembler decode all of those +/// back to the original bytes, so the DWARF payload is byte-exact. +/// +/// This is a security boundary, not cosmetics: a path containing `\"` would +/// otherwise close the directive string early and let the remainder of the path +/// be assembled as directives. fn escape_asm_string(value: &str) -> String { - value.replace('\\', "\\\\").replace('"', "\\\"") + escaped_bytes(value.as_bytes()) +} + +/// Returns whether `symbol` is a plain assembler symbol name, safe to splice +/// into the unquoted `.quad ` operands of a `DW_TAG_subprogram`. +/// +/// Entry symbols are mangled by codegen down to `[A-Za-z0-9_$.]`, so anything +/// else means a malformed or forged `@fn` marker. Such a region is skipped the +/// same way a malformed `@src` marker is: debug-info injection must never fail a +/// build the plain path would accept, and it must never emit an operand that the +/// assembler could read as extra syntax. +fn is_plain_asm_symbol(symbol: &str) -> bool { + !symbol.is_empty() + && symbol + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'.')) } /// Parses a numeric `key=value` token from a marker tail, returning `None` for @@ -240,4 +276,94 @@ _php_foo: assert!(!out.contains(".loc"), "{out}"); assert!(out.contains(" ret\n"), "{out}"); } + + /// Verifies the assembler-string escaper covers every byte class that could + /// terminate or reinterpret a quoted directive operand: backslash, double + /// quote, newline, carriage return, tab, NUL, and non-ASCII UTF-8 bytes. + /// Ordinary path characters must survive verbatim so normal builds are + /// byte-identical to the pre-hardening output. + #[test] + fn escapes_every_assembler_string_metacharacter() { + assert_eq!(escape_asm_string("plain/path-1_2.php"), "plain/path-1_2.php"); + assert_eq!(escape_asm_string("bs\\lash"), "bs\\\\lash"); + assert_eq!(escape_asm_string("q\"uote"), "q\\\"uote"); + assert_eq!(escape_asm_string("a\nb"), "a\\nb"); + assert_eq!(escape_asm_string("a\tb"), "a\\tb"); + assert_eq!(escape_asm_string("a\rb"), "a\\015b"); + assert_eq!(escape_asm_string("a\0b"), "a\\000b"); + assert_eq!(escape_asm_string("é"), "\\303\\251"); + // The historical breakout: `\` was left raw while `"` became `\"`, so the + // pair rendered as an escaped backslash followed by a real closing quote. + assert_eq!(escape_asm_string("a\\\";"), "a\\\\\\\";"); + } + + /// Verifies a source path carrying assembler metacharacters cannot escape + /// any quoted directive: the `.file` header, the compile unit `DW_AT_name`, + /// and the whole module must stay one directive per line. Regression test + /// for the `--debug-info` source-path directive injection. + #[test] + fn source_path_cannot_break_out_of_quoted_directives() { + let path = "sec/a\\\"; .globl pwned; pwned = 7; #\nb\t.php"; + let out = inject_line_directives(ASM, path, Platform::Linux); + + assert_eq!( + out.lines().next().unwrap(), + ".file 1 \"sec/a\\\\\\\"; .globl pwned; pwned = 7; #\\nb\\t.php\"" + ); + assert!( + out.contains(".asciz \"sec/a\\\\\\\"; .globl pwned; pwned = 7; #\\nb\\t.php\""), + "{out}" + ); + for line in out.lines() { + assert!( + !line.starts_with(".globl pwned"), + "path bytes reached the assembler as a directive: {out}" + ); + } + // Every quoted directive must keep an even number of unescaped quotes, + // which is what stops the remainder of the line from being assembled. + for line in out.lines().filter(|line| line.contains('"')) { + let mut quotes = 0usize; + let mut escaped = false; + for byte in line.bytes() { + match (escaped, byte) { + (true, _) => escaped = false, + (false, b'\\') => escaped = true, + (false, b'"') => quotes += 1, + _ => {} + } + } + assert_eq!(quotes % 2, 0, "unbalanced quotes in `{line}`: {out}"); + } + } + + /// Verifies a namespaced PHP function name (which legitimately contains + /// backslashes) is escaped inside its `DW_AT_name` string instead of being + /// emitted raw. + #[test] + fn escapes_namespaced_subprogram_name() { + let asm = " # @fn name=App\\Deep\\greet symbol=_fn_App_N_Deep_N_greet\n\ + _fn_App_N_Deep_N_greet:\n ret\n # @endfn name=App\\Deep\\greet\n"; + let out = inject_line_directives(asm, "a.php", Platform::Linux); + assert!(out.contains(".asciz \"App\\\\Deep\\\\greet\""), "{out}"); + assert!(out.contains(".quad _fn_App_N_Deep_N_greet"), "{out}"); + } + + /// Verifies an `@fn` marker whose entry symbol is not a plain assembler + /// symbol is dropped: `.quad` takes an unquoted expression, so a forged + /// symbol would otherwise splice raw syntax into `.debug_info`. + #[test] + fn skips_subprogram_with_non_symbol_entry() { + let asm = " # @fn name=foo symbol=_ok;.globl_pwned\n_ok:\n ret\n # @endfn name=foo\n"; + let out = inject_line_directives(asm, "a.php", Platform::Linux); + assert!(!out.contains(".quad"), "{out}"); + assert!(!out.contains("Lelephc_fend_0"), "{out}"); + assert!(out.contains(" ret\n"), "{out}"); + + assert!(is_plain_asm_symbol("_fn_App_N_Deep_N_greet")); + assert!(is_plain_asm_symbol("l_.str$1")); + assert!(!is_plain_asm_symbol("")); + assert!(!is_plain_asm_symbol("_ok;.globl x")); + assert!(!is_plain_asm_symbol("_ok\"x")); + } } diff --git a/src/eval_aot/fold_rewrite.rs b/src/eval_aot/fold_rewrite.rs index 043c572b36..e747a40a41 100644 --- a/src/eval_aot/fold_rewrite.rs +++ b/src/eval_aot/fold_rewrite.rs @@ -93,6 +93,7 @@ pub(super) fn fold_static_builtin_calls_in_stmt(stmt: Stmt) -> Stmt { kind, span: stmt.span, source_mode: stmt.source_mode, + strict_types: stmt.strict_types, attributes: stmt.attributes, } } diff --git a/src/func_args/build.rs b/src/func_args/build.rs new file mode 100644 index 0000000000..bd298671e3 --- /dev/null +++ b/src/func_args/build.rs @@ -0,0 +1,202 @@ +//! Purpose: +//! Builds the plain-PHP expressions that replace `func_num_args()`, `func_get_args()` and +//! `func_get_arg($position)` inside a function scope that received the hidden variadic +//! parameter `mixed ...$__elephc_func_args`. +//! +//! Called from: +//! - `crate::func_args::walk::Rewriter`. +//! +//! Key details: +//! - The scopes this pass accepts have only mandatory declared parameters, so every one of +//! them was necessarily passed: `func_num_args()` is exactly +//! ` + count($__elephc_func_args)`, and the argument list is exactly +//! `[$p0, …, $pN-1, ...$__elephc_func_args]`. +//! - `func_get_args()` reports the *current* values of the parameter variables, not the +//! values originally passed (verified against PHP 8.4: reassigning a parameter, or +//! writing through a by-reference parameter, changes what `func_get_args()` returns). +//! Reading the parameter variables at the call point reproduces that exactly. +//! - The array is rebuilt at each use because PHP returns a fresh array every time. +//! - `func_get_arg()` raises `ValueError` — not `ArgumentCountError` — for both a negative +//! position and a position at or past the argument count, with php-src's two distinct +//! messages. + +use crate::names::{Name, NameKind}; +use crate::parser::ast::{BinOp, Expr, ExprKind}; +use crate::span::Span; + +use super::{IntrospectionCall, HIDDEN_ARGS_PARAM, POSITION_TEMP}; + +/// php-src's message when `func_get_arg()` is given a negative position. +const NEGATIVE_POSITION_MESSAGE: &str = + "func_get_arg(): Argument #1 ($position) must be greater than or equal to 0"; + +/// php-src's message when `func_get_arg()` is given a position at or past the number of +/// arguments the current call actually passed. +const OUT_OF_RANGE_POSITION_MESSAGE: &str = + "func_get_arg(): Argument #1 ($position) must be less than the number of the arguments passed to the currently executed function"; + +/// Builds the replacement expression for one introspection call. +/// +/// `param_names` lists the scope's declared regular parameters in order; `args` is the +/// (already rewritten) call-site argument list, which is empty except for +/// `func_get_arg($position)`. +pub(super) fn replacement( + call: IntrospectionCall, + param_names: &[String], + args: &[Expr], + span: Span, +) -> ExprKind { + match call { + IntrospectionCall::NumArgs => argc_expr(param_names, span).kind, + IntrospectionCall::GetArgs => args_array_expr(param_names, span).kind, + IntrospectionCall::GetArg => get_arg_expr(param_names, &args[0], span), + } +} + +/// Builds ` + count($__elephc_func_args)`, or just the `count()` call when +/// the scope declares no regular parameters. +fn argc_expr(param_names: &[String], span: Span) -> Expr { + let surplus = count_call(hidden_args_var(span), span); + if param_names.is_empty() { + return surplus; + } + Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::IntLiteral(param_names.len() as i64), + span, + )), + op: BinOp::Add, + right: Box::new(surplus), + }, + span, + ) +} + +/// Builds `[$p0, …, $pN-1, ...$__elephc_func_args]`, the full argument list in call order. +/// +/// The array literal is fresh at every use, matching PHP's copy semantics, and the spread +/// of the hidden variadic keeps the surplus arguments renumbered from `N`. +fn args_array_expr(param_names: &[String], span: Span) -> Expr { + let mut elements: Vec = param_names + .iter() + .map(|name| Expr::new(ExprKind::Variable(name.clone()), span)) + .collect(); + elements.push(Expr::new( + ExprKind::Spread(Box::new(hidden_args_var(span))), + span, + )); + Expr::new(ExprKind::ArrayLiteral(elements), span) +} + +/// Builds the range-checked indexed read behind `func_get_arg($position)`: +/// +/// ```text +/// $position < 0 +/// ? throw new \ValueError() +/// : ($position < ? [$position] : throw new \ValueError()) +/// ``` +/// +/// The position expression is bound to a hidden local first unless it is already +/// side-effect free, so a call such as `func_get_arg($i++)` evaluates its operand once. +fn get_arg_expr(param_names: &[String], position: &Expr, span: Span) -> ExprKind { + let (first_read, later_read) = position_reads(position, span); + ExprKind::Ternary { + condition: Box::new(less_than( + first_read, + Expr::new(ExprKind::IntLiteral(0), span), + span, + )), + then_expr: Box::new(throw_value_error(NEGATIVE_POSITION_MESSAGE, span)), + else_expr: Box::new(Expr::new( + ExprKind::Ternary { + condition: Box::new(less_than( + later_read.clone(), + argc_expr(param_names, span), + span, + )), + then_expr: Box::new(Expr::new( + ExprKind::ArrayAccess { + array: Box::new(args_array_expr(param_names, span)), + index: Box::new(later_read), + }, + span, + )), + else_expr: Box::new(throw_value_error(OUT_OF_RANGE_POSITION_MESSAGE, span)), + }, + span, + )), + } +} + +/// Returns the two expressions that read the requested position: the first one is +/// evaluated once at the start of the range check, the second is re-read afterwards. +/// +/// A literal or a plain variable is safe to re-evaluate, so it is used directly and no +/// hidden local is introduced. Anything else is bound to `$__elephc_func_arg_pos` by the +/// first read (an assignment expression yields the assigned value in PHP) and re-read from +/// that local, which preserves single evaluation of the operand's side effects. +fn position_reads(position: &Expr, span: Span) -> (Expr, Expr) { + if matches!( + position.kind, + ExprKind::IntLiteral(_) | ExprKind::Variable(_) + ) { + return (position.clone(), position.clone()); + } + let temp = Expr::new(ExprKind::Variable(POSITION_TEMP.to_string()), span); + let bind = Expr::new( + ExprKind::Assignment { + target: Box::new(temp.clone()), + value: Box::new(position.clone()), + result_target: None, + prelude: Vec::new(), + conditional_value_temp: None, + }, + span, + ); + (bind, temp) +} + +/// Builds `$__elephc_func_args`, the hidden variadic parameter holding the surplus +/// positional arguments. +fn hidden_args_var(span: Span) -> Expr { + Expr::new(ExprKind::Variable(HIDDEN_ARGS_PARAM.to_string()), span) +} + +/// Builds `count()`. +fn count_call(value: Expr, span: Span) -> Expr { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("count"), + args: vec![value], + }, + span, + ) +} + +/// Builds ` < `. +fn less_than(left: Expr, right: Expr, span: Span) -> Expr { + Expr::new( + ExprKind::BinaryOp { + left: Box::new(left), + op: BinOp::Lt, + right: Box::new(right), + }, + span, + ) +} + +/// Builds `throw new \ValueError()` as an expression, PHP 8's throw-expression +/// form, so it can sit in a ternary branch. +fn throw_value_error(message: &str, span: Span) -> Expr { + Expr::new( + ExprKind::Throw(Box::new(Expr::new( + ExprKind::NewObject { + class_name: Name::from_parts(NameKind::FullyQualified, vec!["ValueError".to_string()]), + args: vec![Expr::new(ExprKind::StringLiteral(message.to_string()), span)], + }, + span, + ))), + span, + ) +} diff --git a/src/func_args/mod.rs b/src/func_args/mod.rs new file mode 100644 index 0000000000..0d1ffc4faf --- /dev/null +++ b/src/func_args/mod.rs @@ -0,0 +1,235 @@ +//! Purpose: +//! Implements PHP's variadic-argument introspection functions — `func_num_args()`, +//! `func_get_args()` and `func_get_arg($position)` — by desugaring them into ordinary +//! PHP the rest of the compiler already understands. +//! +//! Called from: +//! - `crate::pipeline::compile()`, right after `autoload::run` and before the AST optimizer. +//! +//! Key details: +//! - PHP lets any function be called with more positional arguments than it declares; the +//! surplus is reachable only through these three functions. elephc models that by giving +//! every function whose body uses one of them a hidden trailing variadic parameter +//! `mixed ...$__elephc_func_args`, so the *existing* variadic call machinery (planner, +//! EIR lowering, ABI) packs the surplus with no new ABI surface. +//! - Because the introspection calls are rewritten away here, no builtin registry entry +//! exists for them: they behave like the language constructs PHP itself special-cases +//! (php-src rejects `$f = 'func_num_args'; $f();` with "Cannot call func_num_args() +//! dynamically" for the same reason). +//! - The pass runs *after* name resolution and autoloading so autoloaded declarations are +//! covered too. Call names are therefore matched on their unqualified last segment, +//! case-insensitively, which accepts both the canonical `func_num_args` and the +//! `Foo\func_num_args` a namespaced unqualified call resolves to when no such user +//! function exists. A program that declares its own function with one of the three names +//! disables the pass entirely (see `program_declares_introspection_name`). +//! - Supported scopes are functions, methods (instance and static) and closures/arrow +//! functions whose declared parameters are all mandatory and which declare no variadic of +//! their own. Every other shape is a hard error rather than a silently wrong answer — +//! see `walk::Rewriter::scope_replacement` for the exact diagnostics. + +mod build; +mod walk; + +use crate::errors::CompileError; +use crate::names::Name; +use crate::parser::ast::{ClassMethod, Program, Stmt, StmtKind}; +use crate::types::FunctionSig; + +/// Name of the hidden variadic parameter that collects the surplus positional arguments. +/// +/// Reserved: user code cannot declare `$__elephc_func_args` and reach this slot, and the +/// name never appears in a PHP-visible signature position because it is added after the +/// source declaration has been parsed. +pub(crate) const HIDDEN_ARGS_PARAM: &str = "__elephc_func_args"; + +/// Name of the hidden local that holds the evaluated `func_get_arg()` position when the +/// position expression is not already side-effect free, so it is evaluated exactly once +/// across the range checks and the indexed read. +const POSITION_TEMP: &str = "__elephc_func_arg_pos"; + +/// The three PHP argument-introspection functions this pass rewrites. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum IntrospectionCall { + /// `func_num_args()` — the number of arguments actually passed. + NumArgs, + /// `func_get_args()` — a fresh list of the arguments actually passed. + GetArgs, + /// `func_get_arg($position)` — one argument by zero-based position. + GetArg, +} + +impl IntrospectionCall { + /// Returns the introspection call named by `name`, matching PHP's case-insensitive + /// function names on the unqualified last segment so `func_num_args`, + /// `\func_num_args` and the `Foo\func_num_args` produced by resolving an unqualified + /// call inside a namespace all map to the same construct. + fn from_name(name: &Name) -> Option { + let segment = name.last_segment()?; + Self::from_segment(segment) + } + + /// Returns the introspection call spelled by a single unqualified identifier. + fn from_segment(segment: &str) -> Option { + if segment.eq_ignore_ascii_case("func_num_args") { + Some(Self::NumArgs) + } else if segment.eq_ignore_ascii_case("func_get_args") { + Some(Self::GetArgs) + } else if segment.eq_ignore_ascii_case("func_get_arg") { + Some(Self::GetArg) + } else { + None + } + } + + /// Returns the canonical PHP spelling, used in every diagnostic this pass emits. + fn php_name(self) -> &'static str { + match self { + Self::NumArgs => "func_num_args", + Self::GetArgs => "func_get_args", + Self::GetArg => "func_get_arg", + } + } + + /// Returns how many arguments PHP's signature accepts: none for `func_num_args()` and + /// `func_get_args()`, exactly one (`$position`) for `func_get_arg()`. + fn arity(self) -> usize { + match self { + Self::NumArgs | Self::GetArgs => 0, + Self::GetArg => 1, + } + } +} + +/// Rewrites every supported use of `func_num_args()`, `func_get_args()` and +/// `func_get_arg()` into plain PHP, adding the hidden variadic parameter to each function +/// scope that needed one. +/// +/// Returns the rewritten program, or the combined diagnostics for every unsupported use. +/// A program that declares its own function named after one of the three constructs is +/// returned untouched, so the user's declaration keeps winning exactly as it does in PHP. +pub fn desugar(program: Program) -> Result { + if program_declares_introspection_name(&program) { + return Ok(program); + } + let mut program = program; + let mut rewriter = walk::Rewriter::new(); + rewriter.walk_stmts(&mut program); + match rewriter.into_errors() { + errors if errors.is_empty() => Ok(program), + errors => Err(CompileError::from_many(errors)), + } +} + +/// Returns whether `sig`'s variadic parameter is the hidden one this pass appends rather +/// than a variadic the source function declared. +/// +/// The distinction matters at every call site: PHP accepts unknown *named* arguments for a +/// user-declared variadic (they become string-keyed variadic entries) but rejects them for +/// a function that declares no variadic at all — which, as far as the program is concerned, +/// is exactly what a scope carrying only the hidden parameter still is. +pub(crate) fn sig_collects_surplus_args(sig: &FunctionSig) -> bool { + sig.variadic.as_deref() == Some(HIDDEN_ARGS_PARAM) +} + +/// Returns whether the program declares a function or method named after one of the three +/// introspection constructs. +/// +/// PHP resolves an unqualified call inside a namespace to the namespaced function when one +/// exists, so such a program must keep its own definition. Detecting the name anywhere is +/// deliberately conservative: the cost of a false positive is that the introspection +/// constructs stay unsupported in that one program, which is exactly the behaviour before +/// this pass existed. +fn program_declares_introspection_name(program: &[Stmt]) -> bool { + program.iter().any(stmt_declares_introspection_name) +} + +/// Returns whether a statement — or any statement nested inside it — declares a function +/// or method whose name collides with one of the three introspection constructs. +fn stmt_declares_introspection_name(stmt: &Stmt) -> bool { + match &stmt.kind { + StmtKind::FunctionDecl { name, body, .. } => { + declared_name_collides(name) || body.iter().any(stmt_declares_introspection_name) + } + StmtKind::ClassDecl { methods, .. } + | StmtKind::EnumDecl { methods, .. } + | StmtKind::InterfaceDecl { methods, .. } + | StmtKind::TraitDecl { methods, .. } => methods.iter().any(method_declares_introspection_name), + StmtKind::NamespaceBlock { body, .. } + | StmtKind::Synthetic(body) + | StmtKind::IncludeOnceGuard { body, .. } + | StmtKind::While { body, .. } + | StmtKind::DoWhile { body, .. } + | StmtKind::Foreach { body, .. } => body.iter().any(stmt_declares_introspection_name), + StmtKind::If { + then_body, + elseif_clauses, + else_body, + .. + } => { + then_body.iter().any(stmt_declares_introspection_name) + || elseif_clauses + .iter() + .any(|(_, body)| body.iter().any(stmt_declares_introspection_name)) + || else_body + .as_ref() + .is_some_and(|body| body.iter().any(stmt_declares_introspection_name)) + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + then_body.iter().any(stmt_declares_introspection_name) + || else_body + .as_ref() + .is_some_and(|body| body.iter().any(stmt_declares_introspection_name)) + } + StmtKind::For { + init, update, body, .. + } => { + init.as_deref().is_some_and(stmt_declares_introspection_name) + || update.as_deref().is_some_and(stmt_declares_introspection_name) + || body.iter().any(stmt_declares_introspection_name) + } + StmtKind::Switch { cases, default, .. } => { + cases + .iter() + .any(|(_, body)| body.iter().any(stmt_declares_introspection_name)) + || default + .as_ref() + .is_some_and(|body| body.iter().any(stmt_declares_introspection_name)) + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + try_body.iter().any(stmt_declares_introspection_name) + || catches + .iter() + .any(|catch| catch.body.iter().any(stmt_declares_introspection_name)) + || finally_body + .as_ref() + .is_some_and(|body| body.iter().any(stmt_declares_introspection_name)) + } + // Remaining statements cannot introduce a function declaration. A closure body can, + // but a function declared inside a closure only becomes visible once the closure + // runs, so it can never be the compile-time resolution target of a call this pass + // rewrites. + _ => false, + } +} + +/// Returns whether a class/trait/interface/enum method body declares a function whose name +/// collides with one of the three introspection constructs. The method's own name cannot +/// collide: methods are reached through `->`/`::` call syntax, never as free functions. +fn method_declares_introspection_name(method: &ClassMethod) -> bool { + method.body.iter().any(stmt_declares_introspection_name) +} + +/// Returns whether a declared function name collides with one of the three introspection +/// constructs, comparing the unqualified last segment case-insensitively. +fn declared_name_collides(name: &str) -> bool { + let segment = name.rsplit('\\').next().unwrap_or(name); + IntrospectionCall::from_segment(segment).is_some() +} diff --git a/src/func_args/walk.rs b/src/func_args/walk.rs new file mode 100644 index 0000000000..99418c0c6a --- /dev/null +++ b/src/func_args/walk.rs @@ -0,0 +1,619 @@ +//! Purpose: +//! Walks the whole AST in place, rewriting `func_num_args()`, `func_get_args()` and +//! `func_get_arg()` inside every function scope that supports them and adding that scope's +//! hidden `mixed ...$__elephc_func_args` parameter when at least one call was rewritten. +//! +//! Called from: +//! - `crate::func_args::desugar()`. +//! +//! Key details: +//! - Function scopes nest: a closure declared inside a function has its own argument frame, +//! so `Rewriter::scope` is saved and restored around every function-like node instead of +//! being inherited. +//! - Children are rewritten before their parent node, so `func_get_arg(func_num_args() - 1)` +//! lowers the inner call first and the outer call sees a plain expression. +//! - The statement and expression matches are exhaustive (no wildcard arm). A new AST node +//! must be handled here explicitly; a missed one would silently leave an introspection +//! call unrewritten, and the checker would then report it as an undefined function. +//! - Parameter defaults, class constant initialisers, property defaults and enum case +//! values are PHP constant expressions and cannot contain a function call, so they carry +//! no introspection call to rewrite and are not walked. + +use crate::errors::CompileError; +use crate::names::Name; +use crate::parser::ast::{ + AttributeGroup, CallableTarget, ClassMethod, Expr, ExprKind, InstanceOfTarget, Stmt, StmtKind, + TypeExpr, +}; + +use super::{build, IntrospectionCall, HIDDEN_ARGS_PARAM}; + +/// The argument frame of the function-like scope currently being walked. +struct Scope { + /// Declared regular parameters, in declaration order, without the leading `$`. + param_names: Vec, + /// The first declared parameter that carries a default value, if any. Such a scope + /// cannot tell "passed" from "defaulted" through a variadic tail, so it is rejected. + optional_param: Option, + /// The variadic parameter the source function declares itself, if any. + source_variadic: Option, + /// Set once an introspection call was rewritten in this scope, which is what makes the + /// hidden variadic parameter necessary. + used: bool, + /// Diagnostic label for this scope, e.g. `function 'va'`. + label: String, +} + +/// In-place AST rewriter for the three argument-introspection constructs. +pub(super) struct Rewriter { + scope: Option, + errors: Vec, +} + +impl Rewriter { + /// Creates a rewriter positioned at top level, where no argument frame exists. + pub(super) fn new() -> Self { + Self { + scope: None, + errors: Vec::new(), + } + } + + /// Consumes the rewriter and returns every diagnostic collected during the walk. + pub(super) fn into_errors(self) -> Vec { + self.errors + } + + /// Rewrites every statement in a body. + pub(super) fn walk_stmts(&mut self, stmts: &mut [Stmt]) { + for stmt in stmts.iter_mut() { + self.walk_stmt(stmt); + } + } + + /// Rewrites one statement, recursing into every nested statement and expression. + fn walk_stmt(&mut self, stmt: &mut Stmt) { + match &mut stmt.kind { + // Statements with no expression and no nested body. + StmtKind::Break(_) + | StmtKind::Continue(_) + | StmtKind::IncludeOnceMark { .. } + | StmtKind::NamespaceDecl { .. } + | StmtKind::UseDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::FunctionVariantMark { .. } + | StmtKind::Global { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => {} + + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::Assign { value: expr, .. } + | StmtKind::RefAssign { source: expr, .. } + | StmtKind::TypedAssign { value: expr, .. } + | StmtKind::ArrayPush { value: expr, .. } + | StmtKind::ConstDecl { value: expr, .. } + | StmtKind::ListUnpack { value: expr, .. } + | StmtKind::StaticVar { init: expr, .. } + | StmtKind::Include { path: expr, .. } => self.walk_expr(expr), + + StmtKind::Return(value) => { + if let Some(value) = value { + self.walk_expr(value); + } + } + StmtKind::ArrayAssign { index, value, .. } => { + self.walk_expr(index); + self.walk_expr(value); + } + StmtKind::NestedArrayAssign { target, value } => { + self.walk_expr(target); + self.walk_expr(value); + } + StmtKind::PropertyAssign { object, value, .. } + | StmtKind::PropertyArrayPush { object, value, .. } => { + self.walk_expr(object); + self.walk_expr(value); + } + StmtKind::PropertyArrayAssign { + object, + index, + value, + .. + } => { + self.walk_expr(object); + self.walk_expr(index); + self.walk_expr(value); + } + StmtKind::StaticPropertyAssign { value, .. } + | StmtKind::StaticPropertyArrayPush { value, .. } => self.walk_expr(value), + StmtKind::StaticPropertyArrayAssign { index, value, .. } => { + self.walk_expr(index); + self.walk_expr(value); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + self.walk_expr(condition); + self.walk_stmts(then_body); + for (condition, body) in elseif_clauses.iter_mut() { + self.walk_expr(condition); + self.walk_stmts(body); + } + if let Some(body) = else_body { + self.walk_stmts(body); + } + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + self.walk_stmts(then_body); + if let Some(body) = else_body { + self.walk_stmts(body); + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { body, condition } => { + self.walk_expr(condition); + self.walk_stmts(body); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + self.walk_stmt(init); + } + if let Some(condition) = condition { + self.walk_expr(condition); + } + if let Some(update) = update { + self.walk_stmt(update); + } + self.walk_stmts(body); + } + StmtKind::Foreach { array, body, .. } => { + self.walk_expr(array); + self.walk_stmts(body); + } + StmtKind::Switch { + subject, + cases, + default, + } => { + self.walk_expr(subject); + for (conditions, body) in cases.iter_mut() { + self.walk_exprs(conditions); + self.walk_stmts(body); + } + if let Some(body) = default { + self.walk_stmts(body); + } + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + self.walk_stmts(try_body); + for catch in catches.iter_mut() { + self.walk_stmts(&mut catch.body); + } + if let Some(body) = finally_body { + self.walk_stmts(body); + } + } + StmtKind::Synthetic(body) + | StmtKind::NamespaceBlock { body, .. } + | StmtKind::IncludeOnceGuard { body, .. } => self.walk_stmts(body), + StmtKind::FunctionDecl { + name, + params, + param_attributes, + variadic, + variadic_type, + body, + .. + } => { + let label = format!("function '{}'", name); + self.walk_function_scope( + label, + params, + Some(param_attributes), + variadic, + variadic_type, + body, + ); + } + StmtKind::ClassDecl { + name, + methods, + constants, + properties, + .. + } + | StmtKind::TraitDecl { + name, + methods, + constants, + properties, + .. + } + | StmtKind::InterfaceDecl { + name, + methods, + constants, + properties, + .. + } => { + let _ = (constants, properties); + self.walk_methods(name, methods); + } + StmtKind::EnumDecl { name, methods, .. } => self.walk_methods(name, methods), + } + } + + /// Rewrites every method body of a class, trait, interface or enum. Each method is its + /// own argument frame. + fn walk_methods(&mut self, owner: &str, methods: &mut [ClassMethod]) { + for method in methods.iter_mut() { + let label = format!("method '{}::{}'", owner, method.name); + let ClassMethod { + params, + param_attributes, + variadic, + variadic_type, + body, + .. + } = method; + self.walk_function_scope( + label, + params, + Some(param_attributes), + variadic, + variadic_type, + body, + ); + } + } + + /// Walks a function-like body in its own argument frame and, if the body used one of + /// the introspection constructs, appends the hidden `mixed ...$__elephc_func_args` + /// parameter that collects the surplus positional arguments. + /// + /// `param_attributes` is `None` for closures, whose AST node carries no per-parameter + /// attribute list; for every other scope it is kept aligned with `params` plus the one + /// trailing entry the variadic parameter owns. + fn walk_function_scope( + &mut self, + label: String, + params: &[(String, Option, Option, bool)], + param_attributes: Option<&mut Vec>>, + variadic: &mut Option, + variadic_type: &mut Option, + body: &mut Vec, + ) { + let scope = Scope { + param_names: params.iter().map(|(name, ..)| name.clone()).collect(), + optional_param: params + .iter() + .find(|(_, _, default, _)| default.is_some()) + .map(|(name, ..)| name.clone()), + source_variadic: variadic.clone(), + used: false, + label, + }; + let outer = self.scope.replace(scope); + self.walk_stmts(body); + let scope = std::mem::replace(&mut self.scope, outer) + .expect("function scope was installed before walking the body"); + if !scope.used { + return; + } + *variadic = Some(HIDDEN_ARGS_PARAM.to_string()); + *variadic_type = Some(TypeExpr::Named(Name::unqualified("mixed"))); + if let Some(param_attributes) = param_attributes { + if param_attributes.len() == params.len() { + param_attributes.push(Vec::new()); + } + } + } + + /// Rewrites a list of expressions in source order. + fn walk_exprs(&mut self, exprs: &mut [Expr]) { + for expr in exprs.iter_mut() { + self.walk_expr(expr); + } + } + + /// Rewrites one expression: children first, then the node itself when it is one of the + /// three introspection calls. + fn walk_expr(&mut self, expr: &mut Expr) { + match &mut expr.kind { + // Leaves and identifier-only forms. + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::This + | ExprKind::Variable(_) + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::ConstRef(_) + | ExprKind::MagicConstant(_) + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } => {} + + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::Throw(inner) + | ExprKind::Clone(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::YieldFrom(inner) + | ExprKind::Cast { expr: inner, .. } + | ExprKind::PtrCast { expr: inner, .. } + | ExprKind::ObjectClassName { object: inner } + | ExprKind::PropertyAccess { object: inner, .. } + | ExprKind::NullsafePropertyAccess { object: inner, .. } + | ExprKind::NamedArg { value: inner, .. } => self.walk_expr(inner), + + ExprKind::BinaryOp { left, right, .. } => { + self.walk_expr(left); + self.walk_expr(right); + } + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } => { + self.walk_expr(value); + self.walk_expr(default); + } + ExprKind::Pipe { value, callable } => { + self.walk_expr(value); + self.walk_expr(callable); + } + ExprKind::InstanceOf { value, target } => { + self.walk_expr(value); + if let InstanceOfTarget::Expr(target) = target { + self.walk_expr(target); + } + } + ExprKind::Assignment { + target, + value, + result_target, + prelude, + .. + } => { + self.walk_stmts(prelude); + self.walk_expr(target); + self.walk_expr(value); + if let Some(result_target) = result_target { + self.walk_expr(result_target); + } + } + ExprKind::ArrayAccess { array, index } => { + self.walk_expr(array); + self.walk_expr(index); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + self.walk_expr(condition); + self.walk_expr(then_expr); + self.walk_expr(else_expr); + } + ExprKind::ArrayLiteral(items) => self.walk_exprs(items), + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs.iter_mut() { + self.walk_expr(key); + self.walk_expr(value); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + self.walk_expr(subject); + for (conditions, body) in arms.iter_mut() { + self.walk_exprs(conditions); + self.walk_expr(body); + } + if let Some(default) = default { + self.walk_expr(default); + } + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + self.walk_expr(object); + self.walk_expr(property); + } + ExprKind::MethodCall { object, args, .. } + | ExprKind::NullsafeMethodCall { object, args, .. } => { + self.walk_expr(object); + self.walk_exprs(args); + } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + self.walk_expr(object); + self.walk_expr(method); + self.walk_exprs(args); + } + ExprKind::StaticMethodCall { args, .. } + | ExprKind::NewScopedObject { args, .. } + | ExprKind::NewObject { args, .. } + | ExprKind::ClosureCall { args, .. } => self.walk_exprs(args), + ExprKind::NewDynamic { name_expr, args } => { + self.walk_expr(name_expr); + self.walk_exprs(args); + } + ExprKind::NewDynamicObject { + class_name, args, .. + } => { + self.walk_expr(class_name); + self.walk_exprs(args); + } + ExprKind::ExprCall { callee, args } => { + self.walk_expr(callee); + self.walk_exprs(args); + } + ExprKind::BufferNew { len, .. } => self.walk_expr(len), + ExprKind::Yield { key, value } => { + if let Some(key) = key { + self.walk_expr(key); + } + if let Some(value) = value { + self.walk_expr(value); + } + } + // Transient: the resolver expands `IncludeValue` before this pass runs. Recurse + // into the path expression so the walk stays exhaustive. + ExprKind::IncludeValue { path, .. } => self.walk_expr(path), + ExprKind::FirstClassCallable(target) => { + match target { + CallableTarget::Function(name) => { + if let Some(call) = IntrospectionCall::from_name(name) { + self.errors.push(CompileError::new( + expr.span, + &format!("Cannot call {}() dynamically", call.php_name()), + )); + } + } + CallableTarget::StaticMethod { .. } => {} + CallableTarget::Method { object, .. } => self.walk_expr(object), + } + return; + } + ExprKind::Closure { + params, + variadic, + variadic_type, + body, + .. + } => { + self.walk_function_scope( + "closure".to_string(), + params, + None, + variadic, + variadic_type, + body, + ); + return; + } + ExprKind::FunctionCall { args, .. } => self.walk_exprs(args), + } + + self.try_rewrite_call(expr); + } + + /// Replaces `expr` in place when it is a call to one of the three introspection + /// constructs, recording a diagnostic instead when the enclosing scope cannot support + /// it. Any other expression is left untouched. + fn try_rewrite_call(&mut self, expr: &mut Expr) { + let ExprKind::FunctionCall { name, args } = &expr.kind else { + return; + }; + let Some(call) = IntrospectionCall::from_name(name) else { + return; + }; + let args = args.clone(); + match self.scope_replacement(call, &args, expr.span) { + Ok(kind) => expr.kind = kind, + Err(error) => self.errors.push(error), + } + } + + /// Validates that `call` can be rewritten in the current scope and, if so, marks the + /// scope as needing the hidden variadic parameter and builds the replacement. + /// + /// Every rejected shape produces a diagnostic instead of a silently different answer: + /// PHP's own "must be called from a function context" fatal, and the two argument-frame + /// shapes elephc cannot reconstruct from a variadic tail (optional parameters, whose + /// "passed" vs "defaulted" status is not recoverable, and a source-declared variadic, + /// whose contents the body may have reassigned). + fn scope_replacement( + &mut self, + call: IntrospectionCall, + args: &[Expr], + span: crate::span::Span, + ) -> Result { + if args.len() != call.arity() { + return Err(CompileError::new( + span, + &format!( + "{}() expects {} arguments, got {}", + call.php_name(), + call.arity(), + args.len() + ), + )); + } + if args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_))) + { + return Err(CompileError::new( + span, + &format!( + "{}() does not accept named or unpacked arguments", + call.php_name() + ), + )); + } + let Some(scope) = self.scope.as_mut() else { + return Err(CompileError::new( + span, + &format!( + "{}() must be called from a function context", + call.php_name() + ), + )); + }; + if let Some(variadic) = &scope.source_variadic { + return Err(CompileError::new( + span, + &format!( + "{}() is not supported in {}: it declares the variadic parameter ${} — read that parameter directly", + call.php_name(), + scope.label, + variadic + ), + )); + } + if let Some(optional) = &scope.optional_param { + return Err(CompileError::new( + span, + &format!( + "{}() is not supported in {}: parameter ${} has a default value, so elephc cannot tell a passed argument from a defaulted one", + call.php_name(), + scope.label, + optional + ), + )); + } + scope.used = true; + let param_names = scope.param_names.clone(); + Ok(build::replacement(call, ¶m_names, args, span)) + } +} diff --git a/src/ir/builder.rs b/src/ir/builder.rs index d358945218..d6757fb2ae 100644 --- a/src/ir/builder.rs +++ b/src/ir/builder.rs @@ -71,6 +71,64 @@ impl<'f> Builder<'f> { block_id } + /// Seeds an integer local slot with a constant at the end of the function's entry block. + /// + /// This is the one sanctioned way to add function-wide slot initialization after the + /// entry block has been terminated. Ordinary `emit` refuses to append to a terminated + /// block, but a block's terminator is stored beside its instruction list rather than + /// inside it, so appending here still lands before the branch — and therefore + /// dominates every later use, which is exactly what a slot seed needs. + /// + /// Lowering needs it because a hidden slot can be discovered arbitrarily deep in the + /// body (the internal-array-pointer cursor is created at the first `key`/`next`/… + /// call) while its initial value must not be re-applied on every loop iteration. + /// + /// Both emitted instructions are `NonHeap`, so no ownership or cleanup bookkeeping is + /// disturbed by placing them out of lowering order. + pub fn seed_entry_int_local(&mut self, slot: LocalSlotId, value: i64) { + let entry = self.func.entry; + let block_index = entry.as_raw() as usize; + + let const_inst = InstId::from_raw(self.func.instructions.len() as u32); + let const_value = ValueId::from_raw(self.func.values.len() as u32); + self.func.values.push(Value { + ir_type: IrType::I64, + php_type: PhpType::Int, + def: ValueDef::Instruction { + block: entry, + index: self.func.blocks[block_index].instructions.len() as u32, + inst: const_inst, + }, + ownership: Ownership::NonHeap, + }); + self.func.instructions.push(Instruction::new( + Op::ConstI64, + Vec::new(), + Some(Immediate::I64(value)), + Some(const_value), + IrType::I64, + PhpType::Int, + Ownership::NonHeap, + Op::ConstI64.default_effects(), + None, + )); + self.func.blocks[block_index].instructions.push(const_inst); + + let store_inst = InstId::from_raw(self.func.instructions.len() as u32); + self.func.instructions.push(Instruction::new( + Op::StoreLocal, + vec![const_value], + Some(Immediate::LocalSlot(slot)), + None, + IrType::Void, + PhpType::Void, + Ownership::NonHeap, + Op::StoreLocal.default_effects(), + None, + )); + self.func.blocks[block_index].instructions.push(store_inst); + } + /// Moves the insertion cursor to the end of a block. pub fn position_at_end(&mut self, block: BlockId) { self.assert_block_exists(block); diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 841a122eea..509cf78030 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -168,6 +168,7 @@ pub enum MixedNumericOp { Add, Sub, Mul, + Pow, } /// PHP runtime type category tested by the backend-neutral `TypePredicate` opcode. @@ -208,6 +209,7 @@ impl MixedNumericOp { MixedNumericOp::Add => "add", MixedNumericOp::Sub => "sub", MixedNumericOp::Mul => "mul", + MixedNumericOp::Pow => "pow", } } } @@ -272,6 +274,7 @@ pub enum Op { ICheckedAdd, ICheckedSub, ICheckedMul, + ICheckedPow, IDiv, ISDiv, ISMod, @@ -290,6 +293,7 @@ pub enum Op { FPow, FNeg, MixedNumericBinop, + StrIncDec, ICmp, FCmp, StrEq, @@ -419,6 +423,11 @@ pub enum Op { PropGet, PropInitialized, PropSet, + /// Clears a declared instance-property slot for `unset($obj->prop)`: releases the + /// refcounted payload the slot owned and stamps the uninitialized-typed-property + /// marker, so the property stops being reported by `isset()` and by the + /// descriptor walkers. Operand: object; immediate: property name data id. + PropUnset, /// Loads the raw reference-cell pointer stored in a reference property's slot, /// without dereferencing it. Used to alias a local to `$obj->prop` and to return /// `$this->prop` by reference. Operand: object; immediate: property name data id. @@ -557,12 +566,9 @@ impl Op { | IBitOr | IBitXor | IBitNot - | IShl - | IShrA | FAdd | FSub | FMul - | FDiv | FPow | FNeg | ICmp @@ -587,8 +593,13 @@ impl Op { | Move | Borrow | Nop => E::PURE, - IDiv | ISDiv | ISMod | PtrCheckNonnull => E::MAY_FATAL, - ICheckedAdd | ICheckedSub | ICheckedMul => E::ALLOC_HEAP | E::READS_HEAP, + // PHP 8 raises catchable errors here, so these are never removable, hoistable, + // or CSE-able: `/` and `%` throw `DivisionByZeroError` for a zero divisor and + // `<<` / `>>` throw `ArithmeticError` for a negative shift count. + IDiv | ISDiv | ISMod => E::MAY_FATAL | E::MAY_THROW, + IShl | IShrA | FDiv => E::MAY_THROW, + PtrCheckNonnull => E::MAY_FATAL, + ICheckedAdd | ICheckedSub | ICheckedMul | ICheckedPow => E::ALLOC_HEAP | E::READS_HEAP, ConstEnumCase => E::ALLOC_HEAP, LoadCalledClassId => E::READS_LOCAL, LoadLocal | LoadRefCell | LoadStaticLocal | ClosureCapture => E::READS_LOCAL, @@ -664,7 +675,7 @@ impl Op { LoadArrayElemRefCell => E::READS_HEAP | E::MAY_FATAL, BindRefCellPtr => E::WRITES_LOCAL, ArraySet | HashSet | HashUnset | ArrayPush | HashAppend | OffsetUnset | PropSet - | DynamicPropSet | BufferSet | BufferFree | PackedFieldSet | PtrWrite + | PropUnset | DynamicPropSet | BufferSet | BufferFree | PackedFieldSet | PtrWrite | PtrWriteString => E::WRITES_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, MixedArrayAppend => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, // ALLOC_HEAP because the hash-storage lowering goes through `__rt_hash_set`, which @@ -689,6 +700,10 @@ impl Op { | InstanceOfDynamic | MixedNumericBinop | LooseEq | LooseNotEq | Spaceship => { E::READS_HEAP | E::MAY_DEOPT } + // `++`/`--` on a string reads the operand's payload, may write the shared + // concat scratch while building the carried result, and always allocates the + // boxed Mixed cell the new value is returned in. + StrIncDec => E::READS_HEAP | E::ALLOC_CONCAT | E::ALLOC_HEAP | E::MAY_DEOPT, IterCurrentValueRef | IterNext | IterEnd | GeneratorYield | GeneratorYieldFrom | GeneratorReturn => { E::READS_HEAP | E::WRITES_HEAP | E::MAY_DEOPT } @@ -742,10 +757,22 @@ impl Op { } /// Returns true when the builder may replace the conservative default effects. + /// + /// The arithmetic opcodes below default to `MAY_THROW` because PHP raises a catchable + /// `DivisionByZeroError` / `ArithmeticError` for a zero divisor or a negative shift count. + /// `ir_lower::expr::arithmetic_effects()` drops that bit when the right operand is a literal + /// that rules the error out, so `$x << 3` and `$x / 2` stay removable, hoistable, and + /// CSE-able exactly as they were before the guards existed. pub fn allows_effect_refinement(self) -> bool { matches!( self, - Op::Call + Op::IDiv + | Op::ISDiv + | Op::ISMod + | Op::FDiv + | Op::IShl + | Op::IShrA + | Op::Call | Op::FunctionVariantCall | Op::ClosureBind | Op::LanguageConstructCall @@ -809,6 +836,7 @@ impl Op { ICheckedAdd => "ichecked_add", ICheckedSub => "ichecked_sub", ICheckedMul => "ichecked_mul", + ICheckedPow => "ichecked_pow", IDiv => "idiv", ISDiv => "isdiv", ISMod => "ismod", @@ -827,6 +855,7 @@ impl Op { FPow => "fpow", FNeg => "fneg", MixedNumericBinop => "mixed_numeric_binop", + StrIncDec => "str_inc_dec", ICmp => "icmp", FCmp => "fcmp", StrEq => "str_eq", @@ -934,6 +963,7 @@ impl Op { PropGet => "prop_get", PropInitialized => "prop_initialized", PropSet => "prop_set", + PropUnset => "prop_unset", LoadPropRefCell => "load_prop_ref_cell", LoadArrayElemRefCell => "load_array_elem_ref_cell", BindRefCellPtr => "bind_ref_cell_ptr", diff --git a/src/ir/runtime_call.rs b/src/ir/runtime_call.rs index f6ad27f259..b638610a70 100644 --- a/src/ir/runtime_call.rs +++ b/src/ir/runtime_call.rs @@ -86,12 +86,13 @@ impl RuntimeCallTarget { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UnaryStringRuntime { AddSlashes, - Base64Decode, Base64Encode, BinToHex, HexToBin, HtmlEntityDecode, NlToBr, + QuoteMeta, + QuotedPrintableEncode, RawUrlDecode, RawUrlEncode, StripSlashes, @@ -107,12 +108,13 @@ impl UnaryStringRuntime { pub fn as_eir(self) -> &'static str { match self { UnaryStringRuntime::AddSlashes => "string.add_slashes", - UnaryStringRuntime::Base64Decode => "string.base64_decode", UnaryStringRuntime::Base64Encode => "string.base64_encode", UnaryStringRuntime::BinToHex => "string.bin_to_hex", UnaryStringRuntime::HexToBin => "string.hex_to_bin", UnaryStringRuntime::HtmlEntityDecode => "string.html_entity_decode", UnaryStringRuntime::NlToBr => "string.nl_to_br", + UnaryStringRuntime::QuoteMeta => "string.quote_meta", + UnaryStringRuntime::QuotedPrintableEncode => "string.quoted_printable_encode", UnaryStringRuntime::RawUrlDecode => "string.raw_url_decode", UnaryStringRuntime::RawUrlEncode => "string.raw_url_encode", UnaryStringRuntime::StripSlashes => "string.strip_slashes", diff --git a/src/ir/runtime_fn.rs b/src/ir/runtime_fn.rs index 32aa266d4b..808767eb20 100644 --- a/src/ir/runtime_fn.rs +++ b/src/ir/runtime_fn.rs @@ -53,6 +53,7 @@ pub enum RuntimeFnId { ArrayChunk, ArrayColumn, ArrayCombine, + ArrayCountValues, ArrayDiff, ArrayDiffAssoc, ArrayDiffKey, @@ -75,6 +76,12 @@ pub enum RuntimeFnId { ArrayMultisort, ArrayPad, ArrayPop, + /// Resolves the next internal-array-pointer cursor for `reset`/`end`/`next`/`prev`. + ArrayPtrSeek, + /// Boxes the key at an internal-array-pointer cursor for `key()`. + ArrayPtrKey, + /// Boxes the value at an internal-array-pointer cursor for `current()` and friends. + ArrayPtrValue, ArrayProduct, ArrayPush, ArrayRand, @@ -306,15 +313,21 @@ pub enum RuntimeFnId { Asin, Atan, Atan2, + BaseConvert, Ceil, Clamp, Cos, Cosh, + Bindec, + Decbin, + Dechex, + Decoct, Deg2rad, Exp, Fdiv, Floor, Fmod, + Hexdec, Hypot, Intdiv, Log, @@ -323,6 +336,7 @@ pub enum RuntimeFnId { Max, Min, MtRand, + Octdec, Pi, Pow, Rad2deg, @@ -334,6 +348,10 @@ pub enum RuntimeFnId { Sqrt, Tan, Tanh, + ElephcObjectIsEnum, + ElephcObjectPropCount, + ElephcObjectPropName, + ElephcObjectPropValue, ElephcPtrIsNull, ElephcPtrReadString, ElephcPtrWriteString, @@ -370,8 +388,11 @@ pub enum RuntimeFnId { SplClasses, SplObjectHash, SplObjectId, + Base64Decode, Chop, Chr, + ChunkSplit, + CountChars, Crc32, CtypeAlnum, CtypeAlpha, @@ -419,12 +440,19 @@ pub enum RuntimeFnId { StrReplace, StrSplit, StrStartsWith, + StrWordCount, Strcasecmp, Strcmp, + Strncasecmp, + Strncmp, + Stripos, Strpos, + Strripos, Strrpos, + Strtr, Strstr, Substr, + SubstrCount, SubstrReplace, Trim, Ucfirst, @@ -480,6 +508,7 @@ pub enum RuntimeFnId { GetResourceId, GetResourceType, Gettype, + IntvalBase, IsCallable, IsFinite, IsInfinite, @@ -491,12 +520,15 @@ pub enum RuntimeFnId { impl RuntimeFnId { /// Returns the central logical ABI and backend contract for this runtime function. pub fn descriptor(self) -> RuntimeFnDescriptor { - let logical_signature = crate::builtins::registry::runtime_fn_arity_bounds(self).map( - |(min_operands, max_operands)| crate::ir::RuntimeCallSignature::Polymorphic { - min_operands, - max_operands, - }, - ); + let logical_signature = self + .lowering_owned_arity_bounds() + .or_else(|| crate::builtins::registry::runtime_fn_arity_bounds(self)) + .map( + |(min_operands, max_operands)| crate::ir::RuntimeCallSignature::Polymorphic { + min_operands, + max_operands, + }, + ); RuntimeFnDescriptor { id: self, eir_name: self.as_eir(), @@ -509,6 +541,23 @@ impl RuntimeFnId { } } + /// Returns the operand bounds for runtime functions whose arity is owned by lowering + /// rather than by a PHP builtin's declared parameter list. + /// + /// The registry normally supplies these bounds by reading the declared arity of every + /// builtin that lists the target in its runtime-function inventory. That derivation + /// cannot describe the internal-array-pointer family: `key`/`current`/`next`/`prev`/ + /// `reset`/`end` all take one PHP argument, but their lowering appends the hidden + /// cursor (and, for a seek, the seek mode) as extra operands. Declaring the real + /// runtime arity here keeps EIR validation meaningful instead of switching it off. + const fn lowering_owned_arity_bounds(self) -> Option<(usize, Option)> { + match self { + RuntimeFnId::ArrayPtrSeek => Some((3, Some(3))), + RuntimeFnId::ArrayPtrKey | RuntimeFnId::ArrayPtrValue => Some((2, Some(2))), + _ => None, + } + } + /// Returns representation-safe EIR result metadata when no checked call-site type survives. /// /// Most runtime functions use the registry declaration unchanged. Operations whose registry @@ -524,12 +573,39 @@ impl RuntimeFnId { RuntimeFnId::ArrayKeys | RuntimeFnId::ArraySlice => { PhpType::Array(Box::new(PhpType::Mixed)) } + // The removed-elements array copies the receiver's payload slots, so its element + // layout is the receiver's. A type-changing `$replacement` promotes that receiver to + // `array` during lowering, and the checker's pre-promotion `array` no + // longer describes what the helper produces. + RuntimeFnId::ArraySplice => match arg_types.first().map(PhpType::codegen_repr) { + Some(PhpType::Array(element)) => PhpType::Array(element), + _ => declared.clone(), + }, RuntimeFnId::ArrayValues => match arg_types.first().map(PhpType::codegen_repr) { Some(PhpType::Array(element)) => PhpType::Array(element), Some(PhpType::AssocArray { value, .. }) => PhpType::Array(value), Some(other) => other, None => declared.clone(), }, + // Reversing keeps the container shape, so a synthetic or callable-dispatched + // `array_reverse()` with no checked call-site type still returns concrete array + // metadata. Without it the broad declared `mixed` reached the backend, which stored a + // raw array pointer into a boxed-Mixed slot: `$f = 'array_reverse'; $f([1, 2])` then + // read the pointer as a Mixed cell and crashed. The `$preserve_keys` hash shape needs + // a compile-time literal, which a dynamic wrapper cannot provide, so it is dropped + // from the callable ABI by `refine_runtime_callable_wrapper_sig`. + RuntimeFnId::ArrayReverse => match arg_types.first().map(PhpType::codegen_repr) { + Some(element @ (PhpType::Array(_) | PhpType::AssocArray { .. })) => element, + _ => declared.clone(), + }, + // A synthetic or callable-dispatched `array_chunk()` cannot pass a literal + // `$preserve_keys`, so it always produces the renumbered `array>` nesting. + RuntimeFnId::ArrayChunk => match arg_types.first().map(PhpType::codegen_repr) { + Some(PhpType::Array(element)) => { + PhpType::Array(Box::new(PhpType::Array(element))) + } + _ => declared.clone(), + }, RuntimeFnId::ClassAttributeArgs => PhpType::AssocArray { key: Box::new(PhpType::Mixed), value: Box::new(PhpType::Mixed), @@ -551,6 +627,51 @@ impl RuntimeFnId { } } + /// Reports whether a checked call-site result type is a valid EIR layout for these operands. + /// + /// `BuiltinResultType::Checked` replays the type the checker recorded for one call site, and the + /// checker knows more about a value than EIR does in two routine cases: call-site specialization + /// narrows an untyped parameter (`function top($scores)`) that + /// `eir_signature_with_php_param_contracts` still lowers under the boxed-`Mixed` ABI contract, + /// and a builtin whose EIR result was widened to `array` keeps its precise checker type + /// in the variable that receives it. A runtime function that COPIES an argument's element layout + /// into its result must therefore re-derive that layout from the EIR-visible argument types. + /// Taking the checker's narrower type would describe an array of raw payload pointers where the + /// helper really produced boxed `Mixed` cells, and every later element read would misinterpret + /// them. + /// + /// `array_slice()` and `array_splice()` are the only such targets today, because they are the + /// only copying array helpers with a boxed-`Mixed` lowering; every other runtime function + /// accepts the checked type unchanged. The accepted shapes mirror + /// `require_array_slice_result_type` in the backend: the result element layout must equal the + /// source element layout, or be the `Mixed` widening the lowering emits explicitly. A non-array checked type is the key-preserving hash form, whose + /// values carry the source array's runtime value_type header rather than a copied static + /// element layout, and a source the lowering cannot slice at all is left to the backend so it + /// reports its own diagnostic. Rejecting here makes the caller fall back to + /// `fallback_result_type`, the representation-safe layout the boxed-`Mixed` lowering builds. + pub fn checked_result_type_fits_operands( + self, + arg_types: &[crate::types::PhpType], + checked: &crate::types::PhpType, + ) -> bool { + use crate::types::PhpType; + match self { + RuntimeFnId::ArraySlice | RuntimeFnId::ArraySplice => { + let PhpType::Array(result_element) = checked.codegen_repr() else { + return true; + }; + let source_element = match arg_types.first().map(PhpType::codegen_repr) { + Some(PhpType::Mixed | PhpType::Union(_)) => PhpType::Mixed, + Some(PhpType::Array(element)) => element.codegen_repr(), + _ => return true, + }; + let result_element = result_element.codegen_repr(); + result_element == source_element || result_element == PhpType::Mixed + } + _ => true, + } + } + /// Refines the first-class callable ABI where the direct PHP signature is broader. pub fn refine_first_class_callable_sig(self, sig: &mut crate::types::FunctionSig) { use crate::types::PhpType; @@ -599,6 +720,19 @@ impl RuntimeFnId { use crate::types::PhpType; match self { RuntimeFnId::Count => truncate_callable_params(sig, 1), + // `array_reverse()`'s `$preserve_keys` and `array_slice()`'s `$preserve_keys` pick + // between an indexed array and an integer-keyed hash, so the backend needs them as + // compile-time literals. A dynamic callable wrapper receives runtime parameters, so + // the flag is dropped from the wrapper ABI exactly like `count()`'s `$mode`; the + // wrapper then always produces the renumbered indexed result. `array_slice()`'s + // return type is pinned to the concrete indexed layout its helpers materialize, + // because the wrapper has no per-call-site checked type to read. + RuntimeFnId::ArrayReverse => truncate_callable_params(sig, 1), + RuntimeFnId::ArrayChunk => truncate_callable_params(sig, 2), + RuntimeFnId::ArraySlice => { + truncate_callable_params(sig, 3); + sig.return_type = PhpType::Array(Box::new(PhpType::Mixed)); + } RuntimeFnId::ArraySum | RuntimeFnId::ArrayProduct => { set_callable_param_type(sig, 0, PhpType::Array(Box::new(PhpType::Int))); } @@ -626,13 +760,11 @@ impl RuntimeFnId { match self { RuntimeFnId::Abs | RuntimeFnId::Acos | - RuntimeFnId::ArrayChunk | RuntimeFnId::ArrayColumn | RuntimeFnId::ArrayCombine | RuntimeFnId::ArrayDiff | RuntimeFnId::ArrayDiffAssoc | RuntimeFnId::ArrayDiffKey | - RuntimeFnId::ArrayFill | RuntimeFnId::ArrayFillKeys | RuntimeFnId::ArrayFlip | RuntimeFnId::ArrayIntersect | @@ -645,7 +777,6 @@ impl RuntimeFnId { RuntimeFnId::ArrayKeys | RuntimeFnId::ArrayMerge | RuntimeFnId::ArrayMergeRecursive | - RuntimeFnId::ArrayPad | RuntimeFnId::ArrayProduct | RuntimeFnId::ArrayReplace | RuntimeFnId::ArrayReplaceRecursive | @@ -657,6 +788,11 @@ impl RuntimeFnId { RuntimeFnId::ArrayValues | RuntimeFnId::Asin | RuntimeFnId::Atan | + // `base64_decode()` only reads the subject's bytes and writes its answer into a + // fresh concat reservation; even `$strict = true` reports a bad character as a + // plain `false` return rather than a diagnostic, so nothing observable is lost + // when an unused call is eliminated. + RuntimeFnId::Base64Decode | RuntimeFnId::Atan2 | RuntimeFnId::Ceil | RuntimeFnId::Chop | @@ -668,9 +804,12 @@ impl RuntimeFnId { RuntimeFnId::CtypeAlpha | RuntimeFnId::CtypeDigit | RuntimeFnId::CtypeSpace | + RuntimeFnId::Bindec | + RuntimeFnId::Decbin | + RuntimeFnId::Dechex | + RuntimeFnId::Decoct | RuntimeFnId::Deg2rad | RuntimeFnId::Exp | - RuntimeFnId::Explode | RuntimeFnId::Fdiv | RuntimeFnId::Floor | RuntimeFnId::Fmod | @@ -682,6 +821,7 @@ impl RuntimeFnId { RuntimeFnId::HashEquals | RuntimeFnId::Htmlentities | RuntimeFnId::Htmlspecialchars | + RuntimeFnId::Hexdec | RuntimeFnId::Hypot | RuntimeFnId::Implode | RuntimeFnId::InetNtop | @@ -697,16 +837,13 @@ impl RuntimeFnId { RuntimeFnId::Log2 | RuntimeFnId::Long2ip | RuntimeFnId::Ltrim | - RuntimeFnId::Max | RuntimeFnId::Md5 | - RuntimeFnId::Min | RuntimeFnId::NumberFormat | + RuntimeFnId::Octdec | RuntimeFnId::Ord | RuntimeFnId::Pi | RuntimeFnId::Pow | RuntimeFnId::Rad2deg | - RuntimeFnId::Range | - RuntimeFnId::Round | RuntimeFnId::Rtrim | RuntimeFnId::Sha1 | RuntimeFnId::Sin | @@ -715,15 +852,10 @@ impl RuntimeFnId { RuntimeFnId::StrContains | RuntimeFnId::StrEndsWith | RuntimeFnId::StrIreplace | - RuntimeFnId::StrPad | - RuntimeFnId::StrRepeat | RuntimeFnId::StrReplace | - RuntimeFnId::StrSplit | RuntimeFnId::StrStartsWith | RuntimeFnId::Strcasecmp | RuntimeFnId::Strcmp | - RuntimeFnId::Strpos | - RuntimeFnId::Strrpos | RuntimeFnId::Strstr | RuntimeFnId::Substr | RuntimeFnId::SubstrReplace | @@ -731,9 +863,47 @@ impl RuntimeFnId { RuntimeFnId::Tanh | RuntimeFnId::Trim | RuntimeFnId::Ucfirst | - RuntimeFnId::Ucwords | - RuntimeFnId::Wordwrap => crate::ir::Effects::empty(), - RuntimeFnId::Clamp | RuntimeFnId::ParseUrl => crate::ir::Effects::MAY_THROW, + RuntimeFnId::Ucwords => crate::ir::Effects::empty(), + // These raise reference PHP's catchable `ValueError` for out-of-range + // arguments (`array_chunk()` non-positive length, `clamp()` inverted bounds, + // `array_fill()` negative count, `array_pad()` oversized length, `explode()` + // empty separator, `str_pad()` empty pad string or bad pad type, + // `str_repeat()` negative count, `str_split()` non-positive length, + // `str_word_count()` unknown format, `count_chars()` unknown mode, + // `range()` zero/negative/oversized `$step`, `round()` unknown rounding mode, + // `strncmp()`/`strncasecmp()` negative compare length, + // `strpos()`/`strrpos()`/`stripos()`/`strripos()` `$offset` outside the haystack, + // `substr_count()` empty needle or out-of-subject offset/length, + // `wordwrap()` empty break or zero cutting width, `min()`/`max()` over an + // empty array, `parse_url()` unknown `$component` identifier), so they must not + // be treated + // as removable pure calls: dead-code elimination would drop the diagnostic, and + // the try-prefix hoist would move the call out of the `try` that must catch it. + RuntimeFnId::ArrayChunk + | RuntimeFnId::ArrayFill + | RuntimeFnId::CountChars + | RuntimeFnId::ArrayPad + | RuntimeFnId::Clamp + | RuntimeFnId::Explode + | RuntimeFnId::Max + | RuntimeFnId::Min + | RuntimeFnId::Range + | RuntimeFnId::Round + | RuntimeFnId::StrPad + | RuntimeFnId::StrRepeat + | RuntimeFnId::StrSplit + | RuntimeFnId::StrWordCount + | RuntimeFnId::Strncasecmp + | RuntimeFnId::Strncmp + | RuntimeFnId::Stripos + | RuntimeFnId::Strpos + | RuntimeFnId::Strripos + | RuntimeFnId::Strrpos + | RuntimeFnId::SubstrCount + | RuntimeFnId::BaseConvert + | RuntimeFnId::ChunkSplit + | RuntimeFnId::ParseUrl + | RuntimeFnId::Wordwrap => crate::ir::Effects::MAY_THROW, RuntimeFnId::FunctionExists | RuntimeFnId::Defined | RuntimeFnId::JsonLastError @@ -750,7 +920,14 @@ impl RuntimeFnId { ), RuntimeFnId::GetClass | RuntimeFnId::GetParentClass + | RuntimeFnId::ElephcObjectIsEnum + | RuntimeFnId::ElephcObjectPropCount + | RuntimeFnId::ElephcObjectPropName | RuntimeFnId::SplObjectId => crate::ir::Effects::READS_HEAP, + // Re-boxing a property slot allocates the Mixed cell it hands back. + RuntimeFnId::ElephcObjectPropValue => crate::ir::Effects::from_bits_retain( + crate::ir::Effects::READS_HEAP.bits() | crate::ir::Effects::ALLOC_HEAP.bits(), + ), RuntimeFnId::SplObjectHash => crate::ir::Effects::from_bits_retain( crate::ir::Effects::READS_HEAP.bits() | crate::ir::Effects::ALLOC_CONCAT.bits(), @@ -777,13 +954,30 @@ impl RuntimeFnId { | crate::ir::Effects::MAY_FATAL.bits(), ), RuntimeFnId::Phpversion => crate::ir::Effects::PURE, - RuntimeFnId::MtRand | RuntimeFnId::Rand | RuntimeFnId::RandomInt => { + RuntimeFnId::Rand => crate::ir::Effects::from_bits_retain( + crate::ir::Effects::READS_PROCESS.bits() + | crate::ir::Effects::WRITES_PROCESS.bits(), + ), + // `mt_rand()` and `random_int()` raise a catchable `ValueError` for an inverted + // `[min, max]` range; `rand()` silently swaps the bounds instead. + RuntimeFnId::MtRand | RuntimeFnId::RandomInt => { crate::ir::Effects::from_bits_retain( crate::ir::Effects::READS_PROCESS.bits() - | crate::ir::Effects::WRITES_PROCESS.bits(), + | crate::ir::Effects::WRITES_PROCESS.bits() + | crate::ir::Effects::MAY_THROW.bits(), ) } RuntimeFnId::Sleep | RuntimeFnId::Usleep => crate::ir::Effects::WRITES_PROCESS, + // `intval($value, $base)` only inspects the subject's bytes: the string parser + // allocates nothing, and the boxed-`Mixed` entry point reads the cell before + // handing a non-string payload to the ordinary integer cast. + RuntimeFnId::IntvalBase => crate::ir::Effects::READS_HEAP, + // `strtr()` reads the replacement-pair hash and materializes its result through + // the shared concat reservation front end; it never throws or warns. + RuntimeFnId::Strtr => crate::ir::Effects::from_bits_retain( + crate::ir::Effects::READS_HEAP.bits() + | crate::ir::Effects::ALLOC_CONCAT.bits(), + ), RuntimeFnId::Sprintf | RuntimeFnId::Vsprintf => { crate::ir::Effects::from_bits_retain( crate::ir::Effects::READS_HEAP.bits() @@ -998,6 +1192,13 @@ impl RuntimeFnId { self, ) -> crate::builtins::semantics::BuiltinResultOwnership { use crate::builtins::semantics::BuiltinResultOwnership; + // `intval($value, $base)` hands back a raw machine integer, never storage. Leaving it + // in the default `MayAliasArguments` bucket would keep an owned subject temporary + // alive for the integer's whole lifetime, which is the leak shape already documented + // for `Strpos` and `Strtr` below. + if matches!(self, RuntimeFnId::IntvalBase) { + return BuiltinResultOwnership::NonHeap; + } if matches!( self, RuntimeFnId::Abs @@ -1014,6 +1215,7 @@ impl RuntimeFnId { // temporary, which leaked the whole source table on `array_flip(build())` while // the same call through a named local stayed clean. Its Fresh-owning siblings // `ArrayKeys` / `ArrayValues` were already listed here; this was the gap. + | RuntimeFnId::ArrayCountValues | RuntimeFnId::ArrayFlip | RuntimeFnId::ArrayIntersect | RuntimeFnId::ArrayKeys @@ -1021,6 +1223,11 @@ impl RuntimeFnId { | RuntimeFnId::ArrayMerge | RuntimeFnId::ArrayPad | RuntimeFnId::ArrayPop + // `key()`/`current()` and the seek family all hand back a cell built by + // `__rt_mixed_from_value`, which persists strings and increfs containers, so + // the box is independently owned and never aliases the receiving array. + | RuntimeFnId::ArrayPtrKey + | RuntimeFnId::ArrayPtrValue | RuntimeFnId::ArrayReplace | RuntimeFnId::ArrayReplaceRecursive | RuntimeFnId::ArrayReverse @@ -1028,6 +1235,27 @@ impl RuntimeFnId { | RuntimeFnId::ArraySlice | RuntimeFnId::ArrayUnique | RuntimeFnId::ArrayValues + // `base64_decode()`'s result is `string|false`, so its lowering boxes BOTH + // arms into a fresh Mixed cell and `__rt_mixed_from_value` persists (copies) + // the decoded payload. Nothing handed back points into the encoded subject, + // so the default `MayAliasArguments` bucket would only keep an owned subject + // temporary alive for the boxed result's whole lifetime. + | RuntimeFnId::Base64Decode + // hexdec()/bindec()/octdec() box their `int|float` answer through + // `__rt_mixed_from_value`, so the cell handed back is a fresh allocation + // that cannot alias the parsed subject string. + // Every `count_chars()` shape allocates its own result: modes 0-2 build a + // brand-new tally hash and modes 3-4 hand back a `__rt_str_persist`-owned + // byte list, so nothing returned can alias the subject string. + | RuntimeFnId::CountChars + | RuntimeFnId::Bindec + | RuntimeFnId::Hexdec + | RuntimeFnId::Octdec + // Every property slot is re-boxed through `__rt_mixed_from_value`, + // which persists strings and increfs containers, so the cell handed + // back is independently owned and never aliases the source object's + // storage — the caller may release it like any other temporary. + | RuntimeFnId::ElephcObjectPropValue | RuntimeFnId::Explode | RuntimeFnId::Fgetcsv | RuntimeFnId::FileGetContents @@ -1046,6 +1274,16 @@ impl RuntimeFnId { // `microtime()` formats into fresh storage from the clock; it has no string // argument to alias. Its float mode is non-heap and unaffected. | RuntimeFnId::Microtime + // Every `min()` / `max()` return path materializes fresh storage rather + // than handing back argument storage: the scalar forms return a plain + // register value, a `Mixed` result is boxed through + // `__rt_mixed_from_value` (which persists strings and increfs heap + // children), and the single-array string reduction runs its borrowed + // winner through `__rt_str_persist`. Leaving them in the default + // `MayAliasArguments` bucket suppressed nothing useful and leaked the + // boxed `Mixed` result of `min([...])`. + | RuntimeFnId::Max + | RuntimeFnId::Min | RuntimeFnId::ObGetClean | RuntimeFnId::ObGetContents | RuntimeFnId::ObGetFlush @@ -1065,8 +1303,20 @@ impl RuntimeFnId { | RuntimeFnId::PtrReadString | RuntimeFnId::Range | RuntimeFnId::StrSplit + // Every `str_word_count()` shape allocates its own result: format 0 is a plain + // integer, format 1 pushes persisted copies into a brand-new indexed array, and + // format 2 persists each word before inserting it into a brand-new hash. Nothing + // handed back can alias the subject or the character-list argument. + | RuntimeFnId::StrWordCount + | RuntimeFnId::Stripos | RuntimeFnId::Strpos + | RuntimeFnId::Strripos | RuntimeFnId::Strrpos + // `strtr()` writes into a reservation taken from `__rt_concat_reserve` and then + // copies the finished bytes into owned heap storage through `__rt_str_persist`, + // releasing the reservation afterwards, so the result is caller-owned and can + // never alias the subject, the pair array, or the byte lists. + | RuntimeFnId::Strtr // Strstr's result is `string|false`, so its lowering boxes BOTH arms into a // fresh Mixed cell and `__rt_mixed_from_value` persists (copies) the string // payload — it no longer hands back a borrowed slice of the haystack. Leaving @@ -1081,12 +1331,28 @@ impl RuntimeFnId { // constant 48 bytes, and that `sys_get_temp_dir()` and `tmpfile()` — named // alongside it in that report — are both clean. | RuntimeFnId::Tempnam + // `array_splice()` always answers with the array `__rt_array_new` allocated for + // the removed window; the receiver is mutated through its by-reference slot and + // is never handed back. The default `MayAliasArguments` bucket suppressed the + // release of an owned `$replacement` argument, so `array_splice($a, 1, 2, [9])` + // leaked the literal replacement array on every call. + | RuntimeFnId::ArraySplice | RuntimeFnId::ZvalUnpack ) { BuiltinResultOwnership::Fresh } else if matches!( self, - RuntimeFnId::Htmlentities + RuntimeFnId::BaseConvert + // `__rt_chunk_split` always writes into a reservation taken from + // `__rt_concat_reserve`, so the split result can never alias the subject or + // the separator. The default `MayAliasArguments` bucket kept an owned subject + // temporary alive for the result's whole lifetime, leaking one block per + // `chunk_split(build())` call. + | RuntimeFnId::ChunkSplit + | RuntimeFnId::Decbin + | RuntimeFnId::Dechex + | RuntimeFnId::Decoct + | RuntimeFnId::Htmlentities | RuntimeFnId::Htmlspecialchars | RuntimeFnId::Implode ) { @@ -1111,6 +1377,7 @@ impl RuntimeFnId { RuntimeFnId::ArrayFillKeys => "array_fill_keys", RuntimeFnId::ArrayFilter => "array_filter", RuntimeFnId::ArrayFind => "array_find", + RuntimeFnId::ArrayCountValues => "array_count_values", RuntimeFnId::ArrayFlip => "array_flip", RuntimeFnId::ArrayIntersect => "array_intersect", RuntimeFnId::ArrayIntersectAssoc => "array_intersect_assoc", @@ -1126,6 +1393,9 @@ impl RuntimeFnId { RuntimeFnId::ArrayMultisort => "array_multisort", RuntimeFnId::ArrayPad => "array_pad", RuntimeFnId::ArrayPop => "array_pop", + RuntimeFnId::ArrayPtrSeek => "array_ptr_seek", + RuntimeFnId::ArrayPtrKey => "array_ptr_key", + RuntimeFnId::ArrayPtrValue => "array_ptr_value", RuntimeFnId::ArrayProduct => "array_product", RuntimeFnId::ArrayPush => "array_push", RuntimeFnId::ArrayRand => "array_rand", @@ -1361,11 +1631,17 @@ impl RuntimeFnId { RuntimeFnId::Clamp => "clamp", RuntimeFnId::Cos => "cos", RuntimeFnId::Cosh => "cosh", + RuntimeFnId::Bindec => "bindec", + RuntimeFnId::BaseConvert => "base_convert", + RuntimeFnId::Decbin => "decbin", + RuntimeFnId::Dechex => "dechex", + RuntimeFnId::Decoct => "decoct", RuntimeFnId::Deg2rad => "deg2rad", RuntimeFnId::Exp => "exp", RuntimeFnId::Fdiv => "fdiv", RuntimeFnId::Floor => "floor", RuntimeFnId::Fmod => "fmod", + RuntimeFnId::Hexdec => "hexdec", RuntimeFnId::Hypot => "hypot", RuntimeFnId::Intdiv => "intdiv", RuntimeFnId::Log => "log", @@ -1385,6 +1661,10 @@ impl RuntimeFnId { RuntimeFnId::Sqrt => "sqrt", RuntimeFnId::Tan => "tan", RuntimeFnId::Tanh => "tanh", + RuntimeFnId::ElephcObjectIsEnum => "__elephc_object_is_enum", + RuntimeFnId::ElephcObjectPropCount => "__elephc_object_prop_count", + RuntimeFnId::ElephcObjectPropName => "__elephc_object_prop_name", + RuntimeFnId::ElephcObjectPropValue => "__elephc_object_prop_value", RuntimeFnId::ElephcPtrIsNull => "__elephc_ptr_is_null", RuntimeFnId::ElephcPtrReadString => "__elephc_ptr_read_string", RuntimeFnId::ElephcPtrWriteString => "__elephc_ptr_write_string", @@ -1421,8 +1701,11 @@ impl RuntimeFnId { RuntimeFnId::SplClasses => "spl_classes", RuntimeFnId::SplObjectHash => "spl_object_hash", RuntimeFnId::SplObjectId => "spl_object_id", + RuntimeFnId::Base64Decode => "base64_decode", RuntimeFnId::Chop => "chop", RuntimeFnId::Chr => "chr", + RuntimeFnId::ChunkSplit => "chunk_split", + RuntimeFnId::CountChars => "count_chars", RuntimeFnId::Crc32 => "crc32", RuntimeFnId::CtypeAlnum => "ctype_alnum", RuntimeFnId::CtypeAlpha => "ctype_alpha", @@ -1455,6 +1738,7 @@ impl RuntimeFnId { RuntimeFnId::MbStrlen => "mb_strlen", RuntimeFnId::Md5 => "md5", RuntimeFnId::NumberFormat => "number_format", + RuntimeFnId::Octdec => "octdec", RuntimeFnId::Ord => "ord", RuntimeFnId::ParseUrl => "parse_url", RuntimeFnId::Printf => "printf", @@ -1470,12 +1754,19 @@ impl RuntimeFnId { RuntimeFnId::StrReplace => "str_replace", RuntimeFnId::StrSplit => "str_split", RuntimeFnId::StrStartsWith => "str_starts_with", + RuntimeFnId::StrWordCount => "str_word_count", RuntimeFnId::Strcasecmp => "strcasecmp", RuntimeFnId::Strcmp => "strcmp", + RuntimeFnId::Strncasecmp => "strncasecmp", + RuntimeFnId::Strncmp => "strncmp", + RuntimeFnId::Stripos => "stripos", RuntimeFnId::Strpos => "strpos", + RuntimeFnId::Strripos => "strripos", RuntimeFnId::Strrpos => "strrpos", + RuntimeFnId::Strtr => "strtr", RuntimeFnId::Strstr => "strstr", RuntimeFnId::Substr => "substr", + RuntimeFnId::SubstrCount => "substr_count", RuntimeFnId::SubstrReplace => "substr_replace", RuntimeFnId::Trim => "trim", RuntimeFnId::Ucfirst => "ucfirst", @@ -1531,6 +1822,7 @@ impl RuntimeFnId { RuntimeFnId::GetResourceId => "get_resource_id", RuntimeFnId::GetResourceType => "get_resource_type", RuntimeFnId::Gettype => "gettype", + RuntimeFnId::IntvalBase => "intval_base", RuntimeFnId::IsCallable => "is_callable", RuntimeFnId::IsFinite => "is_finite", RuntimeFnId::IsInfinite => "is_infinite", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 1ef509ee66..fd1bb5323f 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -329,6 +329,9 @@ fn validate_instruction_immediate( MixedNumericBinop => require_immediate(inst_id, inst, "mixed numeric op", |imm| { matches!(imm, Imm::MixedNumericOp(_)) }), + StrIncDec => require_immediate(inst_id, inst, "increment delta", |imm| { + matches!(imm, Imm::I64(1) | Imm::I64(-1)) + }), Cast => require_immediate(inst_id, inst, "cast target", |imm| { matches!(imm, Imm::CastTarget(_)) }), @@ -418,11 +421,14 @@ fn validate_opcode_rules( EvalStaticMethodCall => Ok(()), IAdd | ISub | IMul | IDiv | ISDiv | ISMod | IPow | IBitAnd | IBitOr | IBitXor | IShl | IShrA => check_binary(function, inst_id, inst, IrType::I64, "I64"), - ICheckedAdd | ICheckedSub | ICheckedMul => { + ICheckedAdd | ICheckedSub | ICheckedMul | ICheckedPow => { check_binary(function, inst_id, inst, IrType::I64, "I64") } FAdd | FSub | FMul | FDiv | FPow => check_binary(function, inst_id, inst, IrType::F64, "F64"), MixedNumericBinop => check_count(inst_id, inst, 2, "2"), + // The operand is either a concrete `Str` payload or a boxed Mixed cell, so only + // the arity is pinned here; the backend dispatches on the operand's EIR type. + StrIncDec => check_count(inst_id, inst, 1, "1"), INeg | IBitNot => check_unary(function, inst_id, inst, IrType::I64, "I64"), FNeg => check_unary(function, inst_id, inst, IrType::F64, "F64"), ICmp => check_binary(function, inst_id, inst, IrType::I64, "I64"), diff --git a/src/ir_lower/array_pointer_scan.rs b/src/ir_lower/array_pointer_scan.rs new file mode 100644 index 0000000000..eaf58d78b5 --- /dev/null +++ b/src/ir_lower/array_pointer_scan.rs @@ -0,0 +1,277 @@ +//! Purpose: +//! Pre-declares the hidden internal-array-pointer cursor slots used inside one loop body, +//! before that body is lowered. +//! +//! Called from: +//! - `crate::ir_lower::stmt::lower_stmt()` when it reaches a looping statement. +//! +//! Key details: +//! - WHY THIS EXISTS: `LoweringContext::store_local` rewinds a local's cursor whenever the +//! variable is bound to a different array, but it can only do that once the cursor slot +//! exists, and the slot is created lazily at the variable's first pointer call. Outside a +//! loop, lowering order matches execution order, so a store lowered before the first +//! pointer call also RUNS before it and the entry-block seed of `0` is already correct. +//! Inside a loop that correspondence breaks: `for (...) { $z = [7,8]; ...; next($z); }` +//! lowers the store first, so without this pre-pass the second iteration would inherit +//! the first iteration's cursor. Declaring the slots up front makes the store hook fire. +//! - The scan is deliberately BEST-EFFORT and safe in both directions. Over-approximating +//! costs one unused `i64` frame slot plus one seed store; under-approximating (an +//! expression shape this walker does not recurse into) only restores the behaviour that +//! existed before the pre-pass. Neither can miscompile, which is why the expression walk +//! ends in a catch-all instead of an exhaustive match. +//! - Closure bodies are NOT scanned: a closure is lowered into its own function with its +//! own frame, so its cursors belong to that frame. + +use crate::parser::ast::{Expr, ExprKind, Stmt, StmtKind}; + +use super::context::LoweringContext; + +/// Declares a cursor slot for every plain-variable receiver of an internal-array-pointer +/// builtin appearing inside `body`. +/// +/// Call this before lowering a loop body so that assignments inside the body see an +/// existing cursor slot and emit their rewind. +pub(crate) fn predeclare_loop_cursors(ctx: &mut LoweringContext<'_, '_>, body: &[Stmt]) { + let mut receivers = Vec::new(); + scan_stmts(body, &mut receivers); + for name in receivers { + ctx.array_pointer_cursor_slot(&name); + } +} + +/// Records `name` as a pointer receiver unless it is already known. +fn record(receivers: &mut Vec, name: &str) { + if !receivers.iter().any(|known| known == name) { + receivers.push(name.to_string()); + } +} + +/// Collects pointer receivers from a statement list. +fn scan_stmts(stmts: &[Stmt], receivers: &mut Vec) { + for stmt in stmts { + scan_stmt(stmt, receivers); + } +} + +/// Collects pointer receivers from one statement and every statement list it owns. +fn scan_stmt(stmt: &Stmt, receivers: &mut Vec) { + match &stmt.kind { + StmtKind::Echo(expr) | StmtKind::Throw(expr) | StmtKind::ExprStmt(expr) => { + scan_expr(expr, receivers) + } + StmtKind::Return(expr) => { + if let Some(expr) = expr { + scan_expr(expr, receivers); + } + } + StmtKind::Assign { value, .. } | StmtKind::TypedAssign { value, .. } => { + scan_expr(value, receivers) + } + StmtKind::ArrayAssign { index, value, .. } => { + scan_expr(index, receivers); + scan_expr(value, receivers); + } + StmtKind::ArrayPush { value, .. } => scan_expr(value, receivers), + StmtKind::Synthetic(body) => scan_stmts(body, receivers), + StmtKind::If { + condition, + then_body, + else_body, + .. + } => { + scan_expr(condition, receivers); + scan_stmts(then_body, receivers); + if let Some(body) = else_body { + scan_stmts(body, receivers); + } + } + StmtKind::While { condition, body } + | StmtKind::DoWhile { condition, body } => { + scan_expr(condition, receivers); + scan_stmts(body, receivers); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + scan_stmt(init, receivers); + } + if let Some(condition) = condition { + scan_expr(condition, receivers); + } + if let Some(update) = update { + scan_stmt(update, receivers); + } + scan_stmts(body, receivers); + } + StmtKind::Foreach { array, body, .. } => { + scan_expr(array, receivers); + scan_stmts(body, receivers); + } + StmtKind::Switch { + subject, + cases, + default, + } => { + scan_expr(subject, receivers); + for (case_exprs, body) in cases { + for case in case_exprs { + scan_expr(case, receivers); + } + scan_stmts(body, receivers); + } + if let Some(body) = default { + scan_stmts(body, receivers); + } + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + scan_stmts(try_body, receivers); + for catch in catches { + scan_stmts(&catch.body, receivers); + } + if let Some(body) = finally_body { + scan_stmts(body, receivers); + } + } + // Every other statement either owns no expression that can hold a pointer call in + // this frame (declarations, `break`, `use`, …) or lowers into its own function. + _ => {} + } +} + +/// Collects pointer receivers from one expression and its sub-expressions. +/// +/// The final catch-all is intentional: see the module preamble for why an incomplete walk +/// is safe here. +fn scan_expr(expr: &Expr, receivers: &mut Vec) { + match &expr.kind { + ExprKind::FunctionCall { name, args } => { + if let Some(name) = pointer_receiver_name(name.as_str(), args) { + record(receivers, name); + } + for arg in args { + scan_expr(arg, receivers); + } + } + ExprKind::BinaryOp { left, right, .. } => { + scan_expr(left, receivers); + scan_expr(right, receivers); + } + ExprKind::NullCoalesce { value, default } => { + scan_expr(value, receivers); + scan_expr(default, receivers); + } + ExprKind::Pipe { value, callable } => { + scan_expr(value, receivers); + scan_expr(callable, receivers); + } + 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) => scan_expr(inner, receivers), + ExprKind::Assignment { value, .. } => scan_expr(value, receivers), + ExprKind::Cast { expr: inner, .. } => scan_expr(inner, receivers), + ExprKind::ArrayLiteral(items) => { + for item in items { + scan_expr(item, receivers); + } + } + ExprKind::ArrayLiteralAssoc(entries) => { + for (key, value) in entries { + scan_expr(key, receivers); + scan_expr(value, receivers); + } + } + ExprKind::ArrayAccess { array, index } => { + scan_expr(array, receivers); + scan_expr(index, receivers); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + scan_expr(condition, receivers); + scan_expr(then_expr, receivers); + scan_expr(else_expr, receivers); + } + ExprKind::ShortTernary { value, default } => { + scan_expr(value, receivers); + scan_expr(default, receivers); + } + ExprKind::NamedArg { value, .. } => scan_expr(value, receivers), + ExprKind::MethodCall { object, args, .. } + | ExprKind::NullsafeMethodCall { object, args, .. } => { + scan_expr(object, receivers); + for arg in args { + scan_expr(arg, receivers); + } + } + ExprKind::StaticMethodCall { args, .. } | ExprKind::NewObject { args, .. } => { + for arg in args { + scan_expr(arg, receivers); + } + } + ExprKind::ClosureCall { args, .. } | ExprKind::ExprCall { args, .. } => { + for arg in args { + scan_expr(arg, receivers); + } + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => scan_expr(object, receivers), + ExprKind::Match { + subject, + arms, + default, + } => { + scan_expr(subject, receivers); + for (conditions, body) in arms { + for condition in conditions { + scan_expr(condition, receivers); + } + scan_expr(body, receivers); + } + if let Some(default) = default { + scan_expr(default, receivers); + } + } + // Leaves, declarations, and forms lowered into their own frame (closures) stop the + // walk. Missing a container here only forgoes a pre-declaration; it cannot produce + // a wrong cursor. + _ => {} + } +} + +/// Returns the receiver variable name when `name`/`args` is an internal-array-pointer call. +/// +/// The builtin is recognized through the registry's typed argument-lowering descriptor, the +/// same metadata EIR lowering dispatches on, rather than by matching PHP name strings here. +fn pointer_receiver_name<'a>(name: &str, args: &'a [Expr]) -> Option<&'a str> { + if args.len() != 1 { + return None; + } + let canonical = crate::names::php_symbol_key(name.trim_start_matches('\\')); + let def = crate::builtins::registry::lookup(&canonical)?; + if !matches!( + def.spec.semantics.argument_lowering, + crate::builtins::semantics::BuiltinArgumentLowering::ArrayInternalPointer(_) + ) { + return None; + } + match &args[0].kind { + ExprKind::Variable(variable) => Some(variable.as_str()), + _ => None, + } +} diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 87c57dcdd1..b54d40ea01 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -198,6 +198,10 @@ pub(crate) struct LoweringContext<'m, 'f> { pub builtin_call_types: &'m HashMap, /// Checker-computed fixed-point storage contracts for loop-carried array locals. pub loop_storage_types: &'m crate::types::LoopStorageTypes, + /// Checker-recorded `(scope, local)` pairs for `string` locals used as a `++`/`--` + /// target. Those locals get boxed `Mixed` frame storage from their first store, so + /// every read of the slot is already a boxed load instead of an owned string detach. + pub string_incdec_locals: &'m HashSet<(String, String)>, /// Function-like scope key paired with loop spans for storage-contract lookup. pub loop_storage_scope: String, pub constants: HashMap, @@ -273,6 +277,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { throw_access_sites: &'m HashMap, builtin_call_types: &'m HashMap, loop_storage_types: &'m crate::types::LoopStorageTypes, + string_incdec_locals: &'m HashSet<(String, String)>, loop_storage_scope: String, constants: &'m HashMap, top_level_env: TypeEnv, @@ -305,6 +310,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { throw_access_sites, builtin_call_types, loop_storage_types, + string_incdec_locals, loop_storage_scope, constants: constants.clone(), top_level_env, @@ -639,6 +645,27 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_local_with_kind(name, php_type, LocalKind::PhpLocal) } + /// Returns the frame storage type a local must use, boxing `string` locals that PHP's + /// `++`/`--` can retype. + /// + /// `"9"++` is `int(10)`, so a local the checker recorded as a string increment/decrement + /// target cannot keep concrete `Str` storage. Widening the slot lazily at the increment + /// is not enough: the slot type is a whole-frame property, so every OTHER `Str`-typed + /// read of the same slot would then have to detach an owned copy out of the boxed cell + /// (`__rt_mixed_cast_string`), leaking one heap block per executed read. Boxing from the + /// first store — including the incoming-parameter store — keeps every access on the + /// ordinary boxed-Mixed path instead. + fn boxed_incdec_storage_type(&self, name: &str, php_type: PhpType) -> PhpType { + if !matches!(php_type.codegen_repr(), PhpType::Str) { + return php_type; + } + let key = (self.loop_storage_scope.clone(), name.to_string()); + if self.string_incdec_locals.contains(&key) { + return PhpType::Mixed; + } + php_type + } + /// Declares a local slot with the requested role if it does not already exist. pub(crate) fn declare_local_with_kind( &mut self, @@ -649,13 +676,28 @@ impl<'m, 'f> LoweringContext<'m, 'f> { if let Some(slot) = self.local_slots.get(name) { return *slot; } + let boxed_php_type = if kind == LocalKind::PhpLocal { + self.boxed_incdec_storage_type(name, php_type.clone()) + } else { + php_type.clone() + }; + // An incoming `string` parameter arrives with a `Str` entry already seeded from the + // signature environment, so the boxed contract has to REPLACE that fact rather than + // defer to it; otherwise every read of the parameter stays `Str`-typed against the + // boxed slot the increment needs. + let overrides_seeded_type = boxed_php_type != php_type; + let php_type = boxed_php_type; let ir_type = value_ir_type(&php_type); let slot = self .builder .add_local(Some(name.to_string()), ir_type, php_type.clone(), kind); self.local_slots.insert(name.to_string(), slot); self.local_kinds.insert(name.to_string(), kind); - self.local_types.entry(name.to_string()).or_insert(php_type); + if overrides_seeded_type { + self.local_types.insert(name.to_string(), php_type); + } else { + self.local_types.entry(name.to_string()).or_insert(php_type); + } slot } @@ -742,6 +784,56 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.ref_bound_locals.contains(name) } + /// Returns the hidden internal-array-pointer cursor slot for `variable`, declaring it + /// on first use. + /// + /// PHP keeps the internal pointer on the hashtable itself. elephc's array and hash + /// headers have no room for it — widening either would shift every offset in every + /// runtime helper and inline lowering — so the cursor lives in a hidden `Int` frame + /// slot beside the array local, named after that local so repeated calls on the same + /// variable share one cursor. + /// + /// The slot is seeded with `0` at the END of the entry block, which is where PHP's + /// freshly-created array starts its pointer. Appending there (rather than at the call + /// site) is what makes `while (current($a) !== false) { next($a); }` work: a call-site + /// seed inside a loop body would rewind the cursor on every iteration. The entry block + /// stores its terminator separately from its instruction list, so appending after it + /// has been terminated still lands before the branch and dominates every use. + pub(crate) fn array_pointer_cursor_slot(&mut self, variable: &str) -> LocalSlotId { + let name = Self::array_pointer_cursor_name(variable); + if let Some(slot) = self.local_slots.get(&name) { + return *slot; + } + let slot = self.declare_local_with_kind(&name, PhpType::Int, LocalKind::HiddenTemp); + self.builder.seed_entry_int_local(slot, 0); + self.initialized_slots.insert(slot); + slot + } + + /// Rewinds a local's internal-array-pointer cursor to the first element, if it has one. + /// + /// PHP resets the internal pointer whenever the variable is bound to a different array + /// (`$a = [1,2,3]; next($a); $a = [4,5,6];` leaves `key($a)` at `0`), because the new + /// value carries its own hashtable. Assignments lowered BEFORE the variable's first + /// pointer call see no slot yet and skip this, which is harmless: the cursor is still + /// sitting on its entry-block seed of `0` at that point. + pub(crate) fn reset_array_pointer_cursor(&mut self, variable: &str) { + let name = Self::array_pointer_cursor_name(variable); + let Some(slot) = self.local_slots.get(&name).copied() else { + return; + }; + let zero = self.builder.emit_const_i64(0); + self.builder.emit_store_local(slot, zero); + } + + /// Builds the reserved frame-slot name holding one local's internal-array-pointer cursor. + /// + /// The `__eir_` prefix cannot appear in PHP source, so the cursor can never collide + /// with a user variable. + fn array_pointer_cursor_name(variable: &str) -> String { + format!("__eir_aptr_{}", variable) + } + /// Declares a fresh hidden temporary slot and returns its synthetic name. pub(crate) fn declare_hidden_temp(&mut self, php_type: PhpType) -> String { let name = format!("__eir_tmp{}", self.hidden_temp_counter); @@ -750,6 +842,23 @@ impl<'m, 'f> LoweringContext<'m, 'f> { name } + /// Declares a fresh synthetic slot that follows ordinary PHP local-variable ownership. + /// + /// `declare_hidden_temp` uses `LocalKind::HiddenTemp`, whose store *moves* an already-owned + /// expression result into the slot without retaining it. A lowering that desugars a PHP + /// construct into `$tmp = ; ...; = $tmp;` needs the opposite contract: the + /// read may be a borrowed pointer (a static-property load carries no reference of its own), + /// so the store must retain and function-exit cleanup must release. Declaring the slot as + /// `LocalKind::PhpLocal` gives it exactly the ownership rules the equivalent user-written + /// assignment would have. The `__eir_` prefix cannot appear in PHP source, so the name can + /// never collide with a user variable. + pub(crate) fn declare_synthetic_php_local(&mut self, php_type: PhpType) -> String { + let name = format!("__eir_place{}", self.hidden_temp_counter); + self.hidden_temp_counter += 1; + self.declare_local_with_kind(&name, php_type, LocalKind::PhpLocal); + name + } + /// Declares a one-shot hidden expression-result temporary. pub(crate) fn declare_owned_hidden_temp(&mut self, php_type: PhpType) -> String { let name = format!("__eir_tmp{}", self.hidden_temp_counter); @@ -1269,6 +1378,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { if self.should_store_to_eval_scope(name) { return self.store_eval_scope_name(name, value, span); } + // Binding the variable to a different array gives it a different hashtable, and + // PHP's internal pointer belongs to the hashtable: rewind the hidden cursor so + // `$a = [1,2,3]; next($a); $a = [4,5,6];` leaves `key($a)` at `0` like PHP. + self.reset_array_pointer_cursor(name); let previous_slot = self.local_slots.get(name).copied(); let previous_type = self.local_type(name); let previous_kind = self @@ -1279,6 +1392,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let uses_global = self.uses_global_storage(name, previous_kind); let php_type = if uses_global { self.global_alias_type(name) + } else if previous_kind == LocalKind::PhpLocal { + // A `string` local PHP's `++`/`--` can retype uses boxed Mixed storage from its + // FIRST store, so no read of the slot is ever typed `Str` against boxed storage. + self.boxed_incdec_storage_type(name, php_type) } else { php_type }; @@ -1955,9 +2072,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::HashToMixed | Op::InvokerRefArg | Op::MixedNumericBinop + | Op::StrIncDec | Op::ICheckedAdd | Op::ICheckedSub | Op::ICheckedMul + | Op::ICheckedPow | Op::MixedCastString | Op::StrConcat | Op::StrPersist @@ -2098,7 +2217,14 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn call_result_may_alias_arg(&self, argument: ValueId, result: ValueId) -> bool { if matches!( self.builder.value_defining_op(argument), - Some(Op::MixedNumericBinop | Op::ICheckedAdd | Op::ICheckedSub | Op::ICheckedMul) + Some( + Op::MixedNumericBinop + | Op::StrIncDec + | Op::ICheckedAdd + | Op::ICheckedSub + | Op::ICheckedMul + | Op::ICheckedPow + ) ) { return false; } diff --git a/src/ir_lower/expr/array_builtin_args.rs b/src/ir_lower/expr/array_builtin_args.rs index 4849bde58f..31c9f1bc96 100644 --- a/src/ir_lower/expr/array_builtin_args.rs +++ b/src/ir_lower/expr/array_builtin_args.rs @@ -105,6 +105,11 @@ pub(super) fn lower_builtin_call_args( { lower_user_value_sort_args(ctx, sig, args) } + crate::builtins::semantics::BuiltinArgumentLowering::ArraySplice + if !args.iter().any(is_spread_arg) => + { + lower_array_splice_args(ctx, sig, args) + } _ if !crate::types::call_args::has_named_args(args) && !args.iter().any(is_spread_arg) => { diff --git a/src/ir_lower/expr/assignments.rs b/src/ir_lower/expr/assignments.rs index e165f86a70..13e349ae19 100644 --- a/src/ir_lower/expr/assignments.rs +++ b/src/ir_lower/expr/assignments.rs @@ -252,9 +252,16 @@ pub(super) fn lower_dynamic_property_assign( /// Lowers pre/post increment and decrement expressions. /// -/// PHP integer overflow promotion applies: `PHP_INT_MAX + 1` becomes float. -/// The result is typed Mixed and emitted through a checked helper that -/// returns a boxed Mixed value (int or float) at runtime. +/// Three paths, all of which can retype the local, so all of them store a boxed Mixed: +/// - a `Str` or boxed `Mixed` local goes through [`lower_str_inc_dec`], which applies PHP's +/// string rules (`"az"++` is `"ba"`, `"9"++` is `int(10)`) to a string payload and keeps +/// every other payload on the existing numeric helper; +/// - a `Float` local adds or subtracts exactly `1.0` and stays a float; +/// - an `Int` local uses the checked helper, so PHP's overflow promotion applies +/// (`PHP_INT_MAX + 1` becomes float). +/// +/// The post-forms return the value the local held before the store; the pre-forms re-read +/// the local afterwards. pub(super) fn lower_inc_dec( ctx: &mut LoweringContext<'_, '_>, name: &str, @@ -264,19 +271,13 @@ pub(super) fn lower_inc_dec( ) -> LoweredValue { let old = ctx.load_local(name, Some(expr.span)); let existing_type = ctx.local_type(name); - if matches!(existing_type.codegen_repr(), PhpType::Mixed) { + if matches!(existing_type.codegen_repr(), PhpType::Mixed | PhpType::Str) { let return_old = if post { crate::ir_lower::ownership::acquire_if_refcounted(ctx, old, Some(expr.span)) } else { old }; - let one = lower_int_literal(ctx, 1, expr); - let op = if increment { - MixedNumericOp::Add - } else { - MixedNumericOp::Sub - }; - let new = lower_mixed_numeric_binary(ctx, old, one, op, expr); + let new = lower_str_inc_dec(ctx, old, increment, expr); ctx.store_local(name, new, PhpType::Mixed, Some(expr.span)); return if post { return_old @@ -284,6 +285,9 @@ pub(super) fn lower_inc_dec( ctx.load_local(name, Some(expr.span)) }; } + if matches!(existing_type.codegen_repr(), PhpType::Float) { + return lower_float_inc_dec(ctx, name, increment, post, old, expr); + } let one = lower_int_literal(ctx, 1, expr); let operand = coerce_to_int(ctx, old, expr); let checked_int_local = matches!(existing_type.codegen_repr(), PhpType::Int); diff --git a/src/ir_lower/expr/assoc_array_literals.rs b/src/ir_lower/expr/assoc_array_literals.rs index 9f40e65dd0..264397f07b 100644 --- a/src/ir_lower/expr/assoc_array_literals.rs +++ b/src/ir_lower/expr/assoc_array_literals.rs @@ -267,7 +267,7 @@ pub(super) fn nullsafe_method_call_expr_type_for_ir( } /// Merges associative-array value types for EIR storage metadata. -pub(super) fn merge_ir_assoc_value_type(left: PhpType, right: PhpType) -> PhpType { +pub(crate) fn merge_ir_assoc_value_type(left: PhpType, right: PhpType) -> PhpType { ir_array_storage_type(PhpType::widen_array_branch_element(left, right)) } diff --git a/src/ir_lower/expr/call_arg_coercion.rs b/src/ir_lower/expr/call_arg_coercion.rs index 345ff9fb65..4d570e22f6 100644 --- a/src/ir_lower/expr/call_arg_coercion.rs +++ b/src/ir_lower/expr/call_arg_coercion.rs @@ -47,6 +47,11 @@ pub(super) fn coerce_scalar_arg_to_param_storage( let Some((_, param_ty)) = sig.params.get(index) else { return value; }; + // A by-reference parameter must receive the caller's storage, not a converted temporary, + // so declared-parameter scalar binding never applies to one. The checker keeps those on + // the strict path for the same reason. + let bindable = sig.declared_params.get(index).copied().unwrap_or(false) + && !sig.ref_params.get(index).copied().unwrap_or(false); let param_ty = param_ty.codegen_repr(); if value.ir_type == IrType::I64 && param_ty == PhpType::Float { return coerce_to_float(ctx, value, arg); @@ -55,9 +60,33 @@ pub(super) fn coerce_scalar_arg_to_param_storage( if param_ty == PhpType::Str && matches!(source_ty, PhpType::Mixed | PhpType::Union(_)) { return coerce_to_string(ctx, value, arg); } + if bindable { + if let Some(cast) = crate::types::param_binding::scalar_param_cast(¶m_ty, &source_ty) { + return apply_scalar_param_cast(ctx, cast, value, Some(arg.span)); + } + } value } +/// Applies a declared-parameter scalar binding to an already-lowered argument value. +/// +/// The conversion is the one elephc emits for the equivalent explicit cast, which is why the +/// binding is expressed as a `CastType`: `(string)` and `(bool)` are total over the scalar +/// sources `crate::types::param_binding` admits, so no runtime failure path is needed here. +fn apply_scalar_param_cast( + ctx: &mut LoweringContext<'_, '_>, + cast: CastType, + value: LoweredValue, + span: Option, +) -> LoweredValue { + match cast { + CastType::String => coerce_to_string_at_span(ctx, value, span), + CastType::Bool => lower_truthy_bool(ctx, value, span), + // `param_binding::scalar_param_cast` only ever reports the two total scalar casts. + CastType::Int | CastType::Float | CastType::Array => value, + } +} + /// Normalizes reordered call operands to their declared scalar parameter storage. /// /// Named and spread arguments are evaluated in source order and then reordered, so their @@ -95,6 +124,19 @@ pub(super) fn coerce_operands_to_params( ir_type: ctx.builder.value_type(value), }; operands[index] = coerce_to_string_at_span(ctx, lowered, None).value; + } else if sig.declared_params.get(index).copied().unwrap_or(false) { + // Same declared-parameter scalar binding the positional path applies, run here in + // parameter order because named and spread arguments are lowered in source order + // and only reordered afterwards. + if let Some(cast) = + crate::types::param_binding::scalar_param_cast(¶m_ty, &operand_ty) + { + let lowered = LoweredValue { + value, + ir_type: ctx.builder.value_type(value), + }; + operands[index] = apply_scalar_param_cast(ctx, cast, lowered, None).value; + } } } operands @@ -192,6 +234,7 @@ pub(super) fn by_ref_array_arg_needs_mixed_storage( local_elem.codegen_repr() != PhpType::Mixed } +/// Lowers positional call arguments with omitted optional defaults and variadic tail packing. /// Lowers positional call arguments with omitted optional defaults and variadic tail packing. pub(super) fn lower_args_with_signature( ctx: &mut LoweringContext<'_, '_>, @@ -201,6 +244,8 @@ pub(super) fn lower_args_with_signature( let Some(sig) = sig else { return lower_args(ctx, args); }; + let literal_bound = rewrite_literal_param_bindings(sig, args); + let args = literal_bound.as_deref().unwrap_or(args); if crate::types::call_args::has_named_args(args) { let operands = lower_named_args_with_signature(ctx, sig, args); return coerce_operands_to_params(ctx, sig, operands); diff --git a/src/ir_lower/expr/callable_resolution.rs b/src/ir_lower/expr/callable_resolution.rs index f7a2970f6f..c153ae7467 100644 --- a/src/ir_lower/expr/callable_resolution.rs +++ b/src/ir_lower/expr/callable_resolution.rs @@ -55,7 +55,12 @@ pub(super) fn lower_static_callable_call( source_prefers_extension_builtin(&function_name), ); let operands = lower_builtin_call_args(ctx, &function_name, sig.as_ref(), callback_args); - let php_type = call_return_type(ctx, &function_name, &operands); + let php_type = static_callable_builtin_result_type( + ctx, + &function_name, + &operands, + expr.span, + ); Some(emit_builtin_call_value( ctx, &function_name, diff --git a/src/ir_lower/expr/constants.rs b/src/ir_lower/expr/constants.rs index 22d26d1f1f..4297afc3f8 100644 --- a/src/ir_lower/expr/constants.rs +++ b/src/ir_lower/expr/constants.rs @@ -71,6 +71,45 @@ pub(super) fn lower_static_defined_call( )) } +/// Lowers `constant("NAME")` to exactly the EIR a bare `NAME` reference produces. +/// +/// PHP's `constant()` performs a GLOBAL constant lookup: the name is already fully qualified, +/// so no namespace/`use` resolution applies and a leading `\` is stripped. Reusing +/// [`lower_const_ref`] means the call inherits the constant's real type and value — including +/// the prescanned `define()` metadata recorded earlier in source order — instead of an opaque +/// `mixed`. The checker (`crate::builtins::system::constant`) has already refused a dynamic +/// name, a `Foo::BAR` class constant, and an unknown constant, so this hook only ever sees a +/// resolvable global name. +pub(super) fn lower_static_constant_call( + ctx: &mut LoweringContext<'_, '_>, + name: &Name, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(name.as_str().trim_start_matches('\\')) != "constant" || args.len() != 1 { + return None; + } + let constant_name = static_constant_name_arg(&args[0])?; + let referenced = Name::unqualified(constant_name.trim_start_matches('\\')); + Some(lower_const_ref(ctx, &referenced, expr)) +} + +/// Returns the literal constant name from `constant()`'s single argument. +/// +/// Accepts the positional form and the `name:` named-argument form; every other shape (a +/// runtime string, a spread) yields `None` so the caller falls back to the registry path, +/// which the checker has already rejected. +fn static_constant_name_arg(arg: &Expr) -> Option<&str> { + match &arg.kind { + ExprKind::StringLiteral(value) => Some(value.as_str()), + ExprKind::NamedArg { name, value } if name == "name" => match &value.kind { + ExprKind::StringLiteral(value) => Some(value.as_str()), + _ => None, + }, + _ => None, + } +} + /// Lowers a constant reference through prescanned metadata or global storage fallback. pub(super) fn lower_const_ref( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/ir_lower/expr/function_calls.rs b/src/ir_lower/expr/function_calls.rs index 9d4ce2b6b0..5d05534d29 100644 --- a/src/ir_lower/expr/function_calls.rs +++ b/src/ir_lower/expr/function_calls.rs @@ -15,6 +15,9 @@ pub(super) fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name if let Some(value) = constants::lower_static_defined_call(ctx, name, args, expr) { return value; } + if let Some(value) = constants::lower_static_constant_call(ctx, name, args, expr) { + return value; + } let canonical = name.as_str(); if let Some(value) = lower_lazy_isset(ctx, canonical, args, expr) { return value; @@ -34,6 +37,13 @@ pub(super) fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name if let Some(value) = lower_dynamic_call_user_func_array(ctx, canonical, args, expr) { return value; } + // A mutating builtin whose by-reference array argument is a property, static property, or + // container element is rewritten to `$tmp = ; f($tmp, ...); = $tmp;` before + // any builtin fast path runs, so the rewritten call reaches the local-variable + // by-reference lowering that actually stores the copy-on-write result back. + if let Some(value) = ref_place_args::lower_builtin_ref_place_call(ctx, name, args, expr) { + return value; + } if let Some(value) = lower_static_array_map(ctx, canonical, args, expr) { return value; } @@ -54,6 +64,9 @@ pub(super) fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name if let Some(value) = lower_static_array_push(ctx, canonical, args, expr) { return value; } + if let Some(value) = lower_array_internal_pointer(ctx, canonical, args, expr) { + return value; + } if let Some(value) = lower_static_is_callable(ctx, canonical, args, expr) { return value; } @@ -263,12 +276,43 @@ pub(super) fn emit_builtin_call_value( } /// Resolves a migrated registry builtin's result type from the same descriptor as the checker. +/// +/// Used for a builtin lowered at its own call site, so the checker's per-span result type is +/// authoritative and is passed through to the resolver. pub(super) fn registry_builtin_result_type( ctx: &LoweringContext<'_, '_>, name: &str, args: &[Expr], operands: &[crate::ir::ValueId], span: Span, +) -> Option { + // Synthetic builtin-class and prelude AST nodes share the dummy 0:0 + // span, so the checker map cannot identify an individual call there. + // Use the typed runtime target's representation-safe fallback instead + // of accepting whichever synthetic call last occupied that key. + let checked = if span.line != 0 { + ctx.builtin_call_types + .get(&span) + .map(|checked| normalize_value_php_type(checked.clone())) + } else { + None + }; + resolve_registry_builtin_result_type(ctx, name, args, operands, span, checked) +} + +/// Resolves a registry builtin's result type from its descriptor, its lowered operands, and the +/// checker's result type for this very call when one is available. +/// +/// `checked` must be `None` whenever the caller cannot prove the checker examined *this* builtin +/// at `span`; the resolver then derives a representation-safe type from the typed runtime target +/// instead of trusting a type that may describe a different call. +pub(super) fn resolve_registry_builtin_result_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + operands: &[crate::ir::ValueId], + span: Span, + checked: Option, ) -> Option { let def = crate::builtins::registry::lookup(name)?; let arg_types = operands @@ -283,21 +327,21 @@ pub(super) fn registry_builtin_result_type( }; let resolved = match def.spec.semantics.result_type { crate::builtins::semantics::BuiltinResultType::Checked => { - // Synthetic builtin-class and prelude AST nodes share the dummy 0:0 - // span, so the checker map cannot identify an individual call there. - // Use the typed runtime target's representation-safe fallback instead - // of accepting whichever synthetic call last occupied that key. - if span.line != 0 { - if let Some(checked) = ctx.builtin_call_types.get(&span) { - return Some(normalize_value_php_type(checked.clone())); - } - } let crate::builtins::semantics::BuiltinLowering::Runtime( crate::ir::RuntimeCallTarget::Function(target), ) = def.spec.semantics.lowering else { - return None; + return checked; }; + // The checker types an untyped user-function parameter from its call sites, while EIR + // gives it the dynamic boxed-Mixed ABI contract, so a checked type can describe a + // narrower element layout than the operands actually carry. A runtime target that + // copies an argument's layout rejects such a type and re-derives its own. + if let Some(checked) = checked { + if target.checked_result_type_fits_operands(&arg_types, &checked) { + return Some(checked); + } + } target.fallback_result_type(&arg_types, &def.return_type) } crate::builtins::semantics::BuiltinResultType::Declared => def.return_type.clone(), diff --git a/src/ir_lower/expr/generators.rs b/src/ir_lower/expr/generators.rs index 8ce04227f4..fe442f8d6b 100644 --- a/src/ir_lower/expr/generators.rs +++ b/src/ir_lower/expr/generators.rs @@ -115,12 +115,14 @@ pub(super) fn lower_yield_from_array( Some(span), ); // Re-yield the inner key/value pair through the outer generator. The sent - // value is discarded (arrays ignore it), exactly like a `yield $k => $v;` - // statement. + // value is discarded (arrays ignore it). The `Immediate::Bool(true)` marks + // the yield as *delegated*: PHP forwards `yield from` keys verbatim, so an + // integer key from the inner array must not advance the outer generator's + // implicit-key counter (`yield from [7 => "x"]` leaves it where it was). ctx.emit_value( Op::GeneratorYield, vec![key.value, element.value], - None, + Some(Immediate::Bool(true)), PhpType::Mixed, Op::GeneratorYield.default_effects(), Some(span), diff --git a/src/ir_lower/expr/indexed_array_literals.rs b/src/ir_lower/expr/indexed_array_literals.rs index 594fe99f87..7e32531cd2 100644 --- a/src/ir_lower/expr/indexed_array_literals.rs +++ b/src/ir_lower/expr/indexed_array_literals.rs @@ -477,7 +477,7 @@ pub(super) fn materializable_array_element_type(return_type: PhpType) -> Option< } /// Returns the EIR array storage metadata type, preserving PHP resources. -pub(super) fn ir_array_storage_type(php_type: PhpType) -> PhpType { +pub(crate) fn ir_array_storage_type(php_type: PhpType) -> PhpType { let php_type = normalize_value_php_type(php_type); if matches!(php_type, PhpType::Resource(_)) { php_type @@ -487,7 +487,7 @@ pub(super) fn ir_array_storage_type(php_type: PhpType) -> PhpType { } /// Merges indexed-array element types for EIR storage metadata. -pub(super) fn merge_ir_indexed_element_type(left: PhpType, right: PhpType) -> PhpType { +pub(crate) fn merge_ir_indexed_element_type(left: PhpType, right: PhpType) -> PhpType { ir_array_storage_type(PhpType::widen_array_branch_element(left, right)) } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 937a6ec181..fc48334efd 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -34,6 +34,7 @@ use std::collections::HashSet; mod constants; mod nullsafe_chain; +mod ref_place_args; mod scalar_literals; mod numeric_binary; mod string_concat; @@ -43,6 +44,7 @@ mod lazy_branches; mod pipe; mod assignments; mod function_calls; +use function_calls::resolve_registry_builtin_result_type; mod eval_barriers; mod lazy_isset; mod native_isset; @@ -316,3 +318,761 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::YieldFrom(inner) => lower_yield_from(ctx, inner, expr), } } + +/// Returns the effect set for one arithmetic opcode, dropping `MAY_THROW` when the right +/// operand is a literal that provably cannot raise PHP's arithmetic errors. +/// +/// `Op::default_effects()` is opcode-level and must stay conservative: `/`, `%`, `<<`, and `>>` +/// can all raise a catchable error, so a value produced by them is not removable, hoistable, or +/// CSE-able. That would needlessly pessimize the common `$x << 3` / `$x / 2` shapes, where the +/// literal right operand rules the error out at compile time — the same test the AST effect +/// model (`optimize::effects::binary_op_may_throw`) applies. `MAY_FATAL` is left untouched so +/// the division opcodes keep exactly the impurity they had before the guards existed. +fn arithmetic_effects(op: Op, right: &Expr) -> Effects { + let cannot_raise = match op { + Op::IDiv | Op::ISDiv | Op::ISMod | Op::FDiv => matches!( + &right.kind, + ExprKind::IntLiteral(value) if *value != 0 + ) || matches!( + &right.kind, + ExprKind::FloatLiteral(value) if *value != 0.0 + ), + Op::IShl | Op::IShrA => matches!( + &right.kind, + ExprKind::IntLiteral(value) if *value >= 0 + ), + _ => false, + }; + let effects = op.default_effects(); + if cannot_raise { + effects.difference(Effects::MAY_THROW) + } else { + effects + } +} + +/// Lowers PHP's `++` / `--` on a value that may hold a string at runtime. +/// +/// The operand is either a concrete `Str` load or a boxed `Mixed` load, and the result is +/// always a boxed `Mixed` cell: PHP's string increment can change the value's type +/// (`"9"++` is `int(10)`, `"az"++` is `"ba"`), so no concrete slot can hold both outcomes. +/// The `i64` immediate is the delta the runtime helper applies (`+1` or `-1`). +fn lower_str_inc_dec( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + increment: bool, + expr: &Expr, +) -> LoweredValue { + ctx.emit_value( + Op::StrIncDec, + vec![value.value], + Some(Immediate::I64(if increment { 1 } else { -1 })), + PhpType::Mixed, + Op::StrIncDec.default_effects(), + Some(expr.span), + ) +} + +/// Lowers `++`/`--` on a float local as PHP's `$f = $f ± 1.0`. +/// +/// PHP never promotes or demotes a float here: the local stays a float and the operator +/// adds or subtracts exactly one. Post-forms return the value loaded before the store, +/// pre-forms re-read the local so the new value is the expression's result. +fn lower_float_inc_dec( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + increment: bool, + post: bool, + old: LoweredValue, + expr: &Expr, +) -> LoweredValue { + let one = lower_float_literal(ctx, 1.0, expr); + let op = if increment { Op::FAdd } else { Op::FSub }; + let new = ctx + .builder + .emit_with_effects( + op, + vec![old.value, one.value], + None, + IrType::F64, + PhpType::Float, + Ownership::NonHeap, + op.default_effects(), + Some(expr.span), + ) + .expect("float inc/dec produces a value"); + let new = LoweredValue { value: new, ir_type: IrType::F64 }; + ctx.store_local(name, new, PhpType::Float, Some(expr.span)); + if post { + old + } else { + ctx.load_local(name, Some(expr.span)) + } +} + +/// Resolves the result type of a builtin reached through a resolved static callable binding. +/// +/// A callable binding has already collapsed the source argument list into lowered operands, so the +/// registry descriptor is consulted with an empty AST argument list; builtins whose result type is +/// argument-VALUE dependent therefore fall back to the typed runtime target's +/// representation-safe layout instead of the broad declared `returns` type. Without this, a +/// container-returning builtin invoked as `$f = 'array_slice'; $f($a, 1, 2)` reached the backend +/// typed `mixed` while a direct `array_slice($a, 1, 2)` call reached it typed `array`. +/// +/// The checker's per-span result map is deliberately NOT consulted here. On this path `span` +/// identifies the DISPATCHING expression — `call_user_func(...)` or `$f(...)` — not this builtin, +/// so the type recorded there is the dispatcher's own result. When the checker cannot resolve the +/// callback statically (a variable holding the name, which constant propagation only turns into a +/// literal after checking) that entry is `call_user_func()`'s runtime-opaque `mixed`, and adopting +/// it labels a raw scalar/array return as a boxed Mixed cell: `$g = 'array_reverse'; +/// call_user_func($g, [1, 2, 3])` printed `bool(true)`, and the same shape over a `bool`-returning +/// builtin crashed on the boxed-cell dereference. +fn static_callable_builtin_result_type( + ctx: &LoweringContext<'_, '_>, + name: &str, + operands: &[crate::ir::ValueId], + span: Span, +) -> PhpType { + resolve_registry_builtin_result_type(ctx, name, &[], operands, span, None) + .unwrap_or_else(|| call_return_type(ctx, name, operands)) +} + +/// Lowers `isset($object->declaredProperty)` for a DECLARED (typed) property slot. +/// +/// PHP answers `false` for a typed property that is uninitialized — either because it +/// never got a value or because `unset()` removed it — WITHOUT raising the +/// "must not be accessed before initialization" error that a plain read raises. The +/// slot probe therefore runs first, and the ordinary null-check read is only reached +/// on the initialized branch. +fn lower_initialized_property_isset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + arg: &Expr, +) -> LoweredValue { + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let uninitialized_block = ctx + .builder + .create_named_block("isset.property.uninitialized", Vec::new()); + let read_block = ctx + .builder + .create_named_block("isset.property.read", Vec::new()); + let merge = ctx + .builder + .create_named_block("isset.property.merge", Vec::new()); + let data = ctx.intern_string(property); + let initialized = ctx.emit_value( + Op::PropInitialized, + vec![object.value], + Some(Immediate::Data(data)), + PhpType::Bool, + Op::PropInitialized.default_effects(), + Some(arg.span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: initialized.value, + then_target: read_block, + then_args: Vec::new(), + else_target: uninitialized_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(uninitialized_block); + let false_value = emit_bool_literal(ctx, false, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(read_block); + let read_value = lower_property_get_from_value(ctx, object, property, Op::PropGet, arg); + let is_set = emit_builtin_call_value( + ctx, + "isset", + vec![read_value.value], + PhpType::Int, + arg.span, + None, + ); + let is_set = ctx.truthy_consuming(is_set, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, is_set, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + take_owned_temp(ctx, &temp_name, arg.span) +} + +/// Chooses how to unset an undeclared name on an `#[AllowDynamicProperties]` class. +/// +/// PHP consults `__unset()` for such a name only when the dynamic property is ABSENT at +/// the unset site, and removes the hash entry silently when it is present — a decision +/// that depends on runtime state. A class that declares `__unset()` therefore keeps the +/// explicit unsupported diagnostic instead of silently picking one of the two behaviors; +/// a class without `__unset()` can only ever take the removal path. +fn dynamic_property_unset_action( + ctx: &LoweringContext<'_, '_>, + class_name: &str, +) -> UnsetPropertyAction { + if class_method_signature(ctx, class_name, &php_symbol_key("__unset")).is_some() { + return UnsetPropertyAction::Fallback; + } + UnsetPropertyAction::RemoveDynamic +} + +/// Lowers `array_splice()` operands, promoting a typed receiver whose `$replacement` cannot fit. +/// +/// PHP has no per-array element type, so `$a = [1, 2, 3]; array_splice($a, 1, 1, ["x"])` simply +/// leaves `[1, "x", 3]`. elephc types an indexed array at its payload slot, so the promotion has +/// to reach the receiver LOCAL: `__rt_array_to_mixed` re-boxes every live payload and the slot's +/// storage type widens to `array`, which is the representation the boxed insert helper +/// writes into. Without it the backend would have to store a string pointer/length pair in an +/// 8-byte integer slot, which is why the untyped case used to be an explicit `unsupported` +/// diagnostic instead of a wrong answer. +fn lower_array_splice_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let mut operands = if crate::types::call_args::has_named_args(args) { + lower_args_with_signature(ctx, sig, args) + } else { + lower_positional_builtin_args_with_signature(ctx, sig, args) + }; + widen_array_splice_receiver_for_replacement(ctx, sig, args, &mut operands); + operands +} + +/// Promotes an `array_splice()` receiver local to `array` when `$replacement` retypes it. +/// +/// Runs after the operands are lowered because the decision needs both the receiver's slot +/// element type and the replacement's EIR type, and the conversion itself re-reads the receiver +/// local so it observes any mutation the later arguments performed. +fn widen_array_splice_receiver_for_replacement( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], + operands: &mut [crate::ir::ValueId], +) { + let Some(sig) = sig else { + return; + }; + let Some(replacement) = operands.get(3).copied() else { + return; + }; + let Some((name, span)) = array_splice_receiver_local(ctx, sig, args) else { + return; + }; + let PhpType::Array(elem_ty) = ctx.local_type(&name).codegen_repr() else { + return; + }; + let elem_ty = elem_ty.codegen_repr(); + if elem_ty == PhpType::Mixed { + return; + } + let replacement_ty = ctx.builder.value_php_type(replacement).codegen_repr(); + if array_splice_replacement_fits_receiver(&elem_ty, &replacement_ty) { + return; + } + let array_ty = PhpType::Array(Box::new(PhpType::Mixed)); + let local = ctx.load_local(&name, Some(span)); + let converted = ctx.emit_value( + Op::ArrayToMixed, + vec![local.value], + None, + array_ty.clone(), + Op::ArrayToMixed.default_effects(), + Some(span), + ); + ctx.store_mutated_local(&name, converted, array_ty, Some(span)); + operands[0] = ctx.load_local(&name, Some(span)).value; +} + +/// Returns the plain local variable bound to `array_splice()`'s by-reference receiver. +/// +/// Two receiver shapes are deliberately excluded even though they name a local. A by-reference +/// parameter and a `&$x` binding share storage with a caller slot this function cannot retype, +/// and the hidden `__eir_place` temporary of the property/element rewrite is written back into a +/// place whose declared element type is equally out of reach. Widening either would publish +/// boxed `Mixed` cells through a slot still described as `array`, so both keep the +/// backend's explicit diagnostic instead. +fn array_splice_receiver_local( + ctx: &LoweringContext<'_, '_>, + sig: &FunctionSig, + args: &[Expr], +) -> Option<(String, Span)> { + let receiver = args.iter().enumerate().find_map(|(index, arg)| { + let (param_index, place) = match &arg.kind { + ExprKind::NamedArg { name, value } => ( + sig.params.iter().position(|(param, _)| param == name)?, + value.as_ref(), + ), + _ => (index, arg), + }; + (param_index == 0).then_some(place) + })?; + let ExprKind::Variable(name) = &receiver.kind else { + return None; + }; + if !ctx.has_local_slot(name) || ctx.is_ref_bound_local(name) { + return None; + } + if name.starts_with("__eir_place") { + return None; + } + Some((name.clone(), receiver.span)) +} + +/// Reports whether a `$replacement` can be written into the receiver's existing payload slots. +/// +/// Mirrors the shapes the backend's `SpliceReplacement` classifier accepts, so a call this +/// predicate passes never reaches the `unsupported` arm: an omitted/null/empty replacement +/// inserts nothing, an array of the receiver's own element type is copied verbatim, an array of +/// boxed `Mixed` cells is read back as plain integers for an `int`/`bool` receiver (the shape +/// `[$x + 1]` produces), and a bare scalar of the element type becomes a one-element insertion. +fn array_splice_replacement_fits_receiver(elem_ty: &PhpType, replacement_ty: &PhpType) -> bool { + if matches!(replacement_ty, PhpType::Void | PhpType::Never) { + return true; + } + if let PhpType::Array(inner) = replacement_ty { + let inner = inner.codegen_repr(); + if matches!(inner, PhpType::Void | PhpType::Never) { + return true; + } + if &inner == elem_ty { + return true; + } + return inner == PhpType::Mixed && matches!(elem_ty, PhpType::Int | PhpType::Bool); + } + replacement_ty == elem_ty +} + +/// Replaces arguments whose declared-parameter binding is decided by their literal spelling. +/// +/// Runs before any argument is lowered so a callable-name string never materializes as string +/// storage and a constant bound to `int`/`float` is emitted already coerced. Positional and +/// named arguments are both handled; a spread makes the positional mapping unknowable, so the +/// remaining arguments are left alone. +/// +/// Returns `None` when nothing needed rewriting, which is the overwhelmingly common case. +fn rewrite_literal_param_bindings(sig: &FunctionSig, args: &[Expr]) -> Option> { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let mut rewritten: Option> = None; + let mut positional_idx = 0usize; + let mut positional_known = true; + for (arg_idx, arg) in args.iter().enumerate() { + let (param_idx, value) = match &arg.kind { + ExprKind::Spread(_) => { + positional_known = false; + continue; + } + ExprKind::NamedArg { name, value } => { + let Some(param_idx) = sig + .params + .iter() + .take(regular_param_count) + .position(|(param_name, _)| param_name == name) + else { + continue; + }; + (param_idx, value.as_ref()) + } + _ => { + if !positional_known { + continue; + } + let param_idx = positional_idx; + positional_idx += 1; + (param_idx, arg) + } + }; + if param_idx >= regular_param_count + || !sig.declared_params.get(param_idx).copied().unwrap_or(false) + || sig.ref_params.get(param_idx).copied().unwrap_or(false) + { + continue; + } + let Some((_, param_ty)) = sig.params.get(param_idx) else { + continue; + }; + let Some(bound) = crate::types::param_binding::rewrite_literal_param_binding(param_ty, value) + else { + continue; + }; + let slots = rewritten.get_or_insert_with(|| args.to_vec()); + slots[arg_idx] = match &arg.kind { + ExprKind::NamedArg { name, .. } => Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(bound), + }, + arg.span, + ), + _ => bound, + }; + } + rewritten +} + +/// Lowers `new $class(...)` as a class-name dispatch chain when the call shape needs +/// per-class argument planning. +/// +/// Returns `None` when the raw operand list is already correct for every class the +/// runtime dispatch could select, which keeps today's operand ABI for the common +/// exact-arity positional call. Otherwise the class name is evaluated once into a hidden +/// temporary, compared case-insensitively against each candidate class, and each matching +/// branch lowers a fixed-class `new` so named arguments, defaults for omitted optional +/// parameters, and runtime spreads all go through `plan_call_args`. The final `else` +/// branch keeps the generic dynamic-new opcode so runtime-registry classes, the eval +/// bridge, and PHP's class-not-found fatal behave exactly as before. +fn lower_new_dynamic_planned_dispatch( + ctx: &mut LoweringContext<'_, '_>, + name_expr: &Expr, + args: &[Expr], + expr: &Expr, +) -> Option { + let candidates = dynamic_new_planned_candidate_classes(ctx, args); + if candidates.is_empty() { + return None; + } + + let name_value = lower_expr(ctx, name_expr); + let name_type = match ctx.builder.value_php_type(name_value.value).codegen_repr() { + PhpType::Str => PhpType::Str, + _ => PhpType::Mixed, + }; + let name_temp = ctx.declare_owned_hidden_temp(name_type.clone()); + store_value_into_temp(ctx, &name_temp, name_type.clone(), name_value, expr.span); + let name_var = Expr::new(ExprKind::Variable(name_temp.clone()), name_expr.span); + + let result_temp = ctx.declare_owned_hidden_temp(PhpType::Mixed); + let merge = ctx + .builder + .create_named_block("new.dynamic.planned.merge", Vec::new()); + + for class_name in &candidates { + let match_block = ctx + .builder + .create_named_block("new.dynamic.planned.match", Vec::new()); + let next_block = ctx + .builder + .create_named_block("new.dynamic.planned.next", Vec::new()); + let condition = dynamic_new_class_name_match_expr( + &name_var, + class_name, + name_type == PhpType::Str, + name_expr.span, + ); + let condition = lower_expr(ctx, &condition); + let condition = coerce_to_int_at_span(ctx, condition, Some(expr.span)); + ctx.builder.terminate(Terminator::CondBr { + cond: condition.value, + then_target: match_block, + then_args: Vec::new(), + else_target: next_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(match_block); + let class = Name::unqualified(class_name.clone()); + let object = lower_new_object(ctx, &class, args, expr); + store_value_into_temp(ctx, &result_temp, PhpType::Mixed, object, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(next_block); + } + + let name_value = ctx.load_local(&name_temp, Some(expr.span)); + let fallback = lower_new_dynamic_generic(ctx, name_value, args, expr); + store_value_into_temp(ctx, &result_temp, PhpType::Mixed, fallback, expr.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + ctx.clear_owned_hidden_temp(&name_temp, Some(expr.span)); + Some(take_owned_temp(ctx, &result_temp, expr.span)) +} + +/// Builds the case-insensitive class-name test for one dispatch-chain branch. +/// +/// PHP class names are case-insensitive, so the comparison goes through `strcasecmp`. +/// A class-name expression that is not statically a string is guarded by `is_string` +/// first: `new $object()` and other non-string operands must keep falling through to the +/// generic dynamic-new opcode instead of being stringified here. +fn dynamic_new_class_name_match_expr( + name_var: &Expr, + class_name: &str, + name_is_string: bool, + span: Span, +) -> Expr { + let compare = Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("strcasecmp"), + args: vec![ + name_var.clone(), + Expr::new(ExprKind::StringLiteral(class_name.to_string()), span), + ], + }, + span, + )), + op: BinOp::StrictEq, + right: Box::new(Expr::new(ExprKind::IntLiteral(0), span)), + }, + span, + ); + if name_is_string { + return compare; + } + Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("is_string"), + args: vec![name_var.clone()], + }, + span, + )), + op: BinOp::And, + right: Box::new(compare), + }, + span, + ) +} + +/// Returns the classes whose constructor would receive different arguments than the raw +/// dynamic-new operand list, in deterministic order. +/// +/// Only classes that EIR can construct as a fixed class are considered: compiler-internal +/// classes, runtime-managed builtin classes, and classes without an emitted constructor body +/// keep the generic opcode. A class qualifies when the shared call-argument rules accept the +/// call for its constructor, `lower_args_with_signature` is known to resolve it to exactly one +/// operand per declared parameter, *and* the result differs from the raw source arguments — +/// that is exactly the set of classes the generic opcode's exact-arity candidate match would +/// otherwise silently skip or feed in source order. +fn dynamic_new_planned_candidate_classes( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Vec { + if args.is_empty() { + return Vec::new(); + } + let constructor_key = php_symbol_key("__construct"); + let mut candidates = ctx + .classes + .iter() + .filter(|(class_name, class_info)| { + dynamic_new_class_is_planning_candidate( + ctx, + class_name, + class_info, + &constructor_key, + args, + ) + }) + .map(|(class_name, _)| class_name.clone()) + .collect::>(); + candidates.sort(); + candidates +} + +/// Returns true when a dynamic `new` should construct `class_name` through a fixed-class +/// branch instead of the generic dynamic-new opcode. +fn dynamic_new_class_is_planning_candidate( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + class_info: &crate::types::ClassInfo, + constructor_key: &str, + args: &[Expr], +) -> bool { + if class_info.is_abstract || ctx.enums.contains_key(class_name) { + return false; + } + if php_symbol_key(class_name).starts_with("__elephc") { + return false; + } + if crate::codegen_support::dynamic_new::known_dynamic_new_builtin_class_names() + .contains(&class_name) + { + return false; + } + // Runtime-registered builtin classes carry no source constructor body, so a fixed-class + // `ObjectNew` branch would reference a method symbol EIR never emits. + if class_info.declaration_span == Span::dummy() { + return false; + } + if !class_info + .method_decls + .iter() + .any(|method| php_symbol_key(&method.name) == constructor_key && method.has_body) + { + return false; + } + let Some(sig) = class_info.methods.get(constructor_key) else { + return false; + }; + if sig.variadic.is_some() { + return false; + } + if !dynamic_new_args_need_planning(sig, args) { + return false; + } + dynamic_new_args_lower_to_exact_arity(ctx, sig, args) +} + +/// Returns true when a constructor signature would reshape the source argument list. +/// +/// The generic dynamic-new opcode forwards operands positionally and only selects a +/// candidate whose constructor arity matches the operand count exactly, so any named +/// argument, spread, or omitted optional parameter needs the planned path. +fn dynamic_new_args_need_planning(sig: &FunctionSig, args: &[Expr]) -> bool { + crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + || sig.params.len() != args.len() +} + +/// Returns true when `lower_args_with_signature` resolves this call to exactly one operand +/// per declared constructor parameter. +/// +/// The fixed-class `ObjectNew` opcode is arity-exact, and the shared argument lowering falls +/// back to a raw source-order operand list for call shapes it cannot resolve (dynamic +/// associative spreads, multiple spreads, spreads that do not feed the parameter tail). Those +/// shapes must keep the generic dynamic-new opcode rather than produce a mis-arity call. +fn dynamic_new_args_lower_to_exact_arity( + ctx: &LoweringContext<'_, '_>, + sig: &FunctionSig, + args: &[Expr], +) -> bool { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + if crate::types::call_args::has_named_args(args) { + let Ok(plan) = + crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + args, + args[0].span, + regular_param_count, + false, + true, + &assoc_spread_sources(ctx, args), + ) + else { + return false; + }; + return !plan.has_spread_args() && plan.regular_args.len() == sig.params.len(); + } + if args.iter().any(is_spread_arg) { + return single_trailing_indexed_spread_arg(ctx, args) + .is_some_and(|spread_idx| spread_idx <= regular_param_count); + } + if args.len() > sig.params.len() { + return false; + } + (args.len()..sig.params.len()).all(|index| { + sig.defaults + .get(index) + .is_some_and(|default| default.is_some()) + }) +} + +/// Lowers one of PHP's six internal-array-pointer builtins as a cursor-slot operation. +/// +/// The builtin is selected through the registry's typed +/// `BuiltinArgumentLowering::ArrayInternalPointer(op)` descriptor, never by matching the +/// PHP name here, so the six stay distinguishable as metadata all the way down. +/// +/// The receiver's internal pointer is a hidden `Int` frame slot beside the array local +/// (`LoweringContext::array_pointer_cursor_slot`). A read (`key`/`current`) loads that +/// cursor and boxes the key/value at it; a seek (`next`/`prev`/`reset`/`end`) first calls +/// `ArrayPtrSeek` to compute the new cursor, stores it back, and then boxes the value at +/// the new position — the same two-step shape PHP's own implementations use. +/// +/// Returns `None` for anything this path cannot own (named/spread arguments, a wrong +/// argument count, or a receiver that is not a plain variable) so the generic builtin path +/// still runs. The checker has already rejected those shapes with a source-level +/// diagnostic (`crate::builtins::array::internal_pointer`), so reaching the generic path +/// in a successful compile is not possible. +fn lower_array_internal_pointer( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let canonical = php_symbol_key(name.trim_start_matches('\\')); + let op = match crate::builtins::registry::lookup(&canonical) + .map(|def| def.spec.semantics.argument_lowering) + { + Some(crate::builtins::semantics::BuiltinArgumentLowering::ArrayInternalPointer(op)) => op, + _ => return None, + }; + if args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + let ExprKind::Variable(variable) = &args[0].kind else { + return None; + }; + let variable = variable.clone(); + let container = ctx.load_local(&variable, Some(args[0].span)); + let cursor_slot = ctx.array_pointer_cursor_slot(&variable); + let cursor = ctx + .builder + .emit_load_local(cursor_slot, IrType::I64, PhpType::Int); + let cursor = match op.seek_mode() { + None => cursor, + Some(mode) => { + let mode = ctx.builder.emit_const_i64(mode); + let moved = ctx.emit_value( + Op::RuntimeCall, + vec![container.value, cursor, mode], + Some(Immediate::RuntimeCall(crate::ir::RuntimeCallTarget::Function( + crate::ir::RuntimeFnId::ArrayPtrSeek, + ))), + PhpType::Int, + effects_lookup::runtime_effects(), + Some(expr.span), + ); + ctx.builder.emit_store_local(cursor_slot, moved.value); + moved.value + } + }; + let target = if op.reads_key() { + crate::ir::RuntimeFnId::ArrayPtrKey + } else { + crate::ir::RuntimeFnId::ArrayPtrValue + }; + Some(ctx.emit_value( + Op::RuntimeCall, + vec![container.value, cursor], + Some(Immediate::RuntimeCall( + crate::ir::RuntimeCallTarget::Function(target), + )), + PhpType::Mixed, + effects_lookup::runtime_effects(), + Some(expr.span), + )) +} + +/// Emits the generic runtime class-name dispatch for `new $class(...)`. +/// +/// The class-name operand is already lowered so both the direct path and the +/// planned-dispatch fallback branch can share it. +fn lower_new_dynamic_generic( + ctx: &mut LoweringContext<'_, '_>, + name_value: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let mut operands = vec![name_value.value]; + operands.extend(lower_args(ctx, args)); + ctx.emit_value( + Op::DynamicObjectNewMixed, + operands, + None, + PhpType::Mixed, + Op::DynamicObjectNewMixed.default_effects(), + Some(expr.span), + ) +} + +pub(crate) use indexed_array_literals::ir_array_storage_type; +pub(crate) use assoc_array_literals::merge_ir_assoc_value_type; +pub(crate) use indexed_array_literals::merge_ir_indexed_element_type; diff --git a/src/ir_lower/expr/native_isset.rs b/src/ir_lower/expr/native_isset.rs index 3339561942..ad822fffc8 100644 --- a/src/ir_lower/expr/native_isset.rs +++ b/src/ir_lower/expr/native_isset.rs @@ -197,6 +197,10 @@ pub(super) fn lower_lazy_property_isset_operand( lower_expr(ctx, object); Some(emit_bool_literal(ctx, false, Some(arg.span))) } + IssetPropertyAction::Initialized => { + let object = lower_expr(ctx, object); + Some(lower_initialized_property_isset(ctx, object, property, arg)) + } } } @@ -205,6 +209,9 @@ pub(super) enum IssetPropertyAction { Fallback, Magic, AlwaysFalse, + /// A declared (typed) property slot, which can be uninitialized: probe the slot + /// before reading it so `isset()` never raises the uninitialized-read error. + Initialized, } /// Selects the PHP-visible `isset()` behavior for a statically known object property operand. @@ -222,6 +229,9 @@ pub(super) fn property_isset_action( return Some(IssetPropertyAction::Fallback); } if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + if class_info.visible_property_is_declared(property) { + return Some(IssetPropertyAction::Initialized); + } return Some(IssetPropertyAction::Fallback); } if class_method_signature(ctx, &class_name, &php_symbol_key("__isset")).is_some() { diff --git a/src/ir_lower/expr/numeric_binary.rs b/src/ir_lower/expr/numeric_binary.rs index dd0bf8a774..b8fba56f46 100644 --- a/src/ir_lower/expr/numeric_binary.rs +++ b/src/ir_lower/expr/numeric_binary.rs @@ -56,6 +56,36 @@ pub(super) fn lower_numeric_binary( } } if matches!(op, BinOp::Pow) { + // PHP's `**` is int-preserving (`2 ** 3` is `int(8)`), so an int/int power goes + // through the checked helper that reproduces `zend_pow_function_base`: it keeps an + // `i64` while the value fits and promotes to a double at the exact multiplication + // that overflows, or immediately for a negative exponent. Only that case can be an + // int, and the type checker marks it `Mixed` for the same reason it marks + // overflow-capable `+`/`-`/`*` operands `Mixed`. + if lhs.ir_type == IrType::I64 + && rhs.ir_type == IrType::I64 + && fallback_expr_type(expr) == PhpType::Mixed + { + return ctx.emit_value( + Op::ICheckedPow, + vec![lhs.value, rhs.value], + None, + PhpType::Mixed, + Op::ICheckedPow.default_effects(), + Some(expr.span), + ); + } + // A boxed operand (any non-`I64`/`F64` storage, typically an overflow-capable + // `Mixed` int) keeps the int-preserving behavior through the runtime dispatcher, + // which only takes the integer path when both payloads really are integers. + if should_use_mixed_numeric_binop(lhs.ir_type, rhs.ir_type) { + let result = lower_mixed_numeric_binary(ctx, lhs, rhs, MixedNumericOp::Pow, expr); + release_binary_operand_temporary(ctx, lhs, expr.span); + if rhs.value != lhs.value { + release_binary_operand_temporary(ctx, rhs, expr.span); + } + return result; + } let lhs = coerce_to_float(ctx, lhs, expr); let rhs = coerce_to_float(ctx, rhs, expr); return ctx.emit_value( @@ -75,7 +105,7 @@ pub(super) fn lower_numeric_binary( vec![lhs.value, rhs.value], None, PhpType::Int, - Op::ISMod.default_effects(), + arithmetic_effects(Op::ISMod, right), Some(expr.span), ); } @@ -98,7 +128,7 @@ pub(super) fn lower_numeric_binary( vec![lhs.value, rhs.value], None, PhpType::Int, - iop.default_effects(), + arithmetic_effects(iop, right), Some(expr.span), ); } @@ -122,7 +152,7 @@ pub(super) fn lower_numeric_binary( BinOp::Div => Op::FDiv, _ => Op::RuntimeCall, }; - return ctx.emit_value(fop, vec![lhs.value, rhs.value], None, PhpType::Float, fop.default_effects(), Some(expr.span)); + return ctx.emit_value(fop, vec![lhs.value, rhs.value], None, PhpType::Float, arithmetic_effects(fop, right), Some(expr.span)); } if matches!(op, BinOp::Div) && (lhs.ir_type != IrType::I64 || rhs.ir_type != IrType::I64) { let lhs = coerce_to_float(ctx, lhs, left); @@ -132,7 +162,7 @@ pub(super) fn lower_numeric_binary( vec![lhs.value, rhs.value], None, PhpType::Float, - Op::FDiv.default_effects(), + arithmetic_effects(Op::FDiv, right), Some(expr.span), ); } @@ -190,7 +220,7 @@ pub(super) fn lower_numeric_binary( let ownership = Ownership::for_php_type(&php_type); let value = ctx .builder - .emit_with_effects(iop, vec![lhs.value, rhs.value], None, result_type, php_type, ownership, iop.default_effects(), Some(expr.span)) + .emit_with_effects(iop, vec![lhs.value, rhs.value], None, result_type, php_type, ownership, arithmetic_effects(iop, right), Some(expr.span)) .expect("numeric binary produces a value"); return LoweredValue { value, ir_type: result_type }; } @@ -338,6 +368,7 @@ pub(super) fn mixed_numeric_op(op: &BinOp) -> Option { BinOp::Add => Some(MixedNumericOp::Add), BinOp::Sub => Some(MixedNumericOp::Sub), BinOp::Mul => Some(MixedNumericOp::Mul), + BinOp::Pow => Some(MixedNumericOp::Pow), _ => None, } } diff --git a/src/ir_lower/expr/object_construction.rs b/src/ir_lower/expr/object_construction.rs index d30b991dae..919a6ee7a0 100644 --- a/src/ir_lower/expr/object_construction.rs +++ b/src/ir_lower/expr/object_construction.rs @@ -331,15 +331,47 @@ pub(super) fn reflection_parameter_lowered_object_class_name( } /// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. +/// +/// Arguments go through the same shared normalization as every other call surface first: +/// statically-known spreads are flattened to positional/named arguments (`f(...[1, 2])` +/// behaves like `f(1, 2)` and `f(...["a" => 1])` like `f(a: 1)`). The generic dynamic-new +/// opcode passes operands straight through to a runtime class-name dispatch that matches +/// candidates by exact constructor arity, so any call shape that needs per-class planning +/// (named arguments, omitted optional parameters, runtime spreads) is lowered as an +/// explicit class-name dispatch chain instead, where each branch constructs a fixed class +/// through `lower_new_object` and therefore reuses `plan_call_args` in full. pub(super) fn lower_new_dynamic( ctx: &mut LoweringContext<'_, '_>, name_expr: &Expr, args: &[Expr], expr: &Expr, ) -> LoweredValue { - let mut operands = vec![lower_expr(ctx, name_expr).value]; - let uses_runtime_arg_container = args.iter().any(is_spread_arg) - || crate::types::call_args::has_named_args(args); + let args = expand_static_call_spread_args(args); + if let Some(value) = lower_new_dynamic_planned_dispatch(ctx, name_expr, &args, expr) { + return value; + } + let name_value = lower_expr(ctx, name_expr); + lower_new_dynamic_generic(ctx, name_value, &args, expr) +} + +/// Emits the generic runtime class-name dispatch for `new $class(...)`. +/// +/// The class-name operand is already lowered so both the direct path and the +/// planned-dispatch fallback branch can share it. +/// +/// Static spread flattening and planned dispatch run before this, so most call shapes +/// arrive as plain positional arguments. What survives both — a spread whose operand is +/// only known at runtime, or named arguments the planner could not resolve to a class — +/// is passed through the runtime argument container rather than dropped on the floor. +fn lower_new_dynamic_generic( + ctx: &mut LoweringContext<'_, '_>, + name_value: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let mut operands = vec![name_value.value]; + let uses_runtime_arg_container = + args.iter().any(is_spread_arg) || crate::types::call_args::has_named_args(args); if uses_runtime_arg_container { let arg_container = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) .expect("dynamic constructor arguments always have a runtime container form"); diff --git a/src/ir_lower/expr/ref_place_args.rs b/src/ir_lower/expr/ref_place_args.rs new file mode 100644 index 0000000000..d79ca617e0 --- /dev/null +++ b/src/ir_lower/expr/ref_place_args.rs @@ -0,0 +1,359 @@ +//! Purpose: +//! Lowers a mutating builtin call whose by-reference array argument is a *place* other than +//! a plain local — an object property, a static property, or a container element — as an +//! explicit read/mutate/write-back sequence through a hidden temporary. +//! +//! Called from: +//! - `crate::ir_lower::expr::lower_function_call()`, before the builtin fast paths, so the +//! rewritten call re-enters the ordinary local-variable by-reference lowering. +//! +//! Key details: +//! - Only a receiver the backend can resolve to a slot reaches its COW write-back +//! (`ReceiverPlace` in `crate::codegen::lower_inst::receiver_place`), and a plain local +//! variable is the only argument shape that produces one. +//! A property or element operand is loaded, `acquire`d, handed to the runtime, and released, +//! so `__rt_array_ensure_unique` separates a private copy that nothing ever stores back. +//! That is a silent wrong answer: `usort($obj->items, ...)` used to leave `$obj->items` +//! untouched with no diagnostic. +//! - The rewrite is `$tmp = ; f($tmp, ...); = $tmp;`, where `$tmp` is a +//! synthetic slot with ordinary PHP local ownership (`declare_synthetic_php_local`), so the +//! store retains even when the place read is a borrowed pointer — a static-property load +//! carries no reference of its own, and moving it into a hidden temp would let the +//! write-back's release of the previous occupant free an array another variable still holds. +//! - Because `$tmp` retains, the runtime's ensure-unique sees a shared buffer and separates +//! before mutating — which is exactly PHP's copy-on-write behavior: an earlier +//! `$c = $obj->items;` alias stays unsorted, and a `usort` comparator that reads the +//! property while sorting still sees the pre-sort array. +//! - Only array/hash-typed places are rewritten. Scalar by-reference parameters (`settype`, +//! `preg_match` `$matches`, `str_replace` `$count`) keep their existing lowering and their +//! existing diagnostics, because a hidden temp declared with the place's scalar type cannot +//! represent a builtin that re-types its argument. +//! - Place types are resolved statically (no IR is emitted before the decision), so a shape +//! this module cannot resolve falls through to the pre-existing lowering unchanged. + +use crate::ir_lower::context::{LoweredValue, LoweringContext}; +use crate::names::Name; +use crate::parser::ast::{Expr, ExprKind}; +use crate::types::{FunctionSig, PhpType}; + +use super::{ + call_signature, is_spread_arg, lower_expr, lower_function_call, + lower_non_local_assignment_write, normalize_value_php_type, source_prefers_extension_builtin, + static_property_result_type, +}; + +/// One by-reference argument rewritten into a hidden temporary. +/// +/// `place` is the stabilized target expression written back after the call; `temp` is the +/// hidden local holding the mutated array while the builtin runs. +struct RefPlacePlan { + index: usize, + place: Expr, + temp: String, +} + +/// Lowers a builtin call whose by-reference array argument is a non-local place. +/// +/// Returns `None` — leaving the call to the ordinary lowering — unless the callee is a +/// registry builtin with a by-reference regular parameter and at least one such argument is a +/// statically array-typed property, static property, or container element. On a rewrite the +/// place is read into a hidden temporary, the call is re-lowered against that temporary, and +/// the temporary is written back to the place so the caller's storage observes the mutation. +pub(super) fn lower_builtin_ref_place_call( + ctx: &mut LoweringContext<'_, '_>, + name: &Name, + args: &[Expr], + expr: &Expr, +) -> Option { + let canonical = name.as_str(); + let prefer_extension = source_prefers_extension_builtin(canonical); + if !prefer_extension + && (ctx.functions.contains_key(canonical) || ctx.extern_functions.contains_key(canonical)) + { + // User-defined and extern callees own a separate by-reference machine that already + // rejects non-local arguments with a named diagnostic. + return None; + } + let sig = call_signature(ctx, canonical, prefer_extension)?; + if !sig.ref_params.iter().any(|is_ref| *is_ref) { + return None; + } + if args.iter().any(is_spread_arg) { + // A spread cannot be split into per-parameter places here; PHP also rejects spreading + // into a by-reference parameter, and the checker already reports that. + return None; + } + let rewrite_indices: Vec = args + .iter() + .enumerate() + .filter(|(index, arg)| { + ref_param_place(&sig, *index, arg).is_some_and(|place| is_array_place(ctx, place)) + }) + .map(|(index, _)| index) + .collect(); + if rewrite_indices.is_empty() { + return None; + } + let mut call_args: Vec = args.to_vec(); + let mut plans: Vec = Vec::with_capacity(rewrite_indices.len()); + for index in rewrite_indices { + let arg = &args[index]; + let place_arg = ref_param_place(&sig, index, arg)?; + let place = stabilize_place(ctx, place_arg); + let read = lower_expr(ctx, &place); + let value_type = normalize_value_php_type(ctx.builder.value_php_type(read.value)); + let temp = ctx.declare_synthetic_php_local(value_type.clone()); + ctx.store_local(&temp, read, value_type, Some(place_arg.span)); + let variable = Expr::new(ExprKind::Variable(temp.clone()), place_arg.span); + call_args[index] = match &arg.kind { + ExprKind::NamedArg { name, .. } => Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(variable), + }, + arg.span, + ), + _ => variable, + }; + plans.push(RefPlacePlan { index, place, temp }); + } + // Every rewritten argument now names a plain local (directly, or as the value of the named + // argument it replaced), so the recursive call takes the ordinary by-reference path and + // this rewrite cannot re-fire. + debug_assert!(plans.iter().all(|plan| { + let rewritten = &call_args[plan.index]; + let place = match &rewritten.kind { + ExprKind::NamedArg { value, .. } => value.as_ref(), + _ => rewritten, + }; + matches!(place.kind, ExprKind::Variable(_)) + })); + let result = lower_function_call(ctx, name, &call_args, expr); + for plan in plans { + let value = Expr::new(ExprKind::Variable(plan.temp), plan.place.span); + lower_non_local_assignment_write(ctx, &plan.place, &value, plan.place.span); + } + Some(result) +} + +/// Returns the argument expression bound to a by-reference parameter, or `None`. +/// +/// A positional argument binds to the parameter at the same index; a named argument +/// (`sort(array: $obj->items)`) binds to the parameter its name selects, so both call forms +/// reach the same rewrite. Variadic tail positions are excluded because only the visible +/// regular parameters carry the registry's by-reference markers. +fn ref_param_place<'a>(sig: &FunctionSig, index: usize, arg: &'a Expr) -> Option<&'a Expr> { + let regular_param_count = crate::types::call_args::regular_param_count(sig); + let (param_index, place) = match &arg.kind { + ExprKind::NamedArg { name, value } => ( + sig.params.iter().position(|(param, _)| param == name)?, + value.as_ref(), + ), + _ => (index, arg), + }; + if param_index >= regular_param_count { + return None; + } + if !sig.ref_params.get(param_index).copied().unwrap_or(false) { + return None; + } + Some(place) +} + +/// Returns whether a by-reference argument is a non-local place holding array storage. +/// +/// Plain locals are excluded because the existing lowering already writes the separated array +/// back to their frame slot. Scalar places are excluded so builtins that re-type their +/// by-reference argument keep their current lowering and diagnostics. +fn is_array_place(ctx: &LoweringContext<'_, '_>, arg: &Expr) -> bool { + if !is_candidate_place_shape(arg) { + return false; + } + static_place_type(ctx, arg).is_some_and(|php_type| { + matches!( + php_type.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) + }) +} + +/// Returns whether an argument has one of the place shapes this rewrite can read and write. +fn is_candidate_place_shape(arg: &Expr) -> bool { + matches!( + arg.kind, + ExprKind::PropertyAccess { .. } + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::ArrayAccess { .. } + ) +} + +/// Resolves the static PHP type of a place expression without emitting any IR. +/// +/// Only the shapes this module can read and write back are resolved — locals, `$this`, +/// declared instance properties, declared static properties, and elements of those. Anything +/// else returns `None`, which keeps the call on its pre-existing lowering path. +fn static_place_type(ctx: &LoweringContext<'_, '_>, expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Variable(name) => { + if ctx.has_local_slot(name) { + Some(ctx.local_type(name)) + } else { + None + } + } + ExprKind::This => { + if ctx.has_local_slot("this") { + Some(ctx.local_type("this")) + } else { + None + } + } + ExprKind::PropertyAccess { object, property } => { + let class_name = place_object_class_name(ctx, object)?; + let class_info = ctx.classes.get(class_name.as_str())?; + let (_, (_, property_ty)) = class_info.visible_property(property)?; + Some(normalize_value_php_type(property_ty.clone())) + } + ExprKind::StaticPropertyAccess { receiver, property } => Some( + static_property_result_type(ctx, receiver, property, expr), + ), + ExprKind::ArrayAccess { array, .. } => { + match static_place_type(ctx, array)?.codegen_repr() { + PhpType::Array(elem_ty) => Some(normalize_value_php_type(*elem_ty)), + PhpType::AssocArray { value, .. } => Some(normalize_value_php_type(*value)), + _ => None, + } + } + _ => None, + } +} + +/// Resolves the class a property receiver refers to, for property-type lookup. +/// +/// Returns `None` for a receiver whose static type is not a single known class — `Mixed`, +/// a union, or an unresolved local — so the caller leaves the argument on its existing path. +fn place_object_class_name(ctx: &LoweringContext<'_, '_>, object: &Expr) -> Option { + match static_place_type(ctx, object)?.codegen_repr() { + PhpType::Object(class_name) => Some(class_name.trim_start_matches('\\').to_string()), + _ => None, + } +} + +/// Rebuilds a place expression so it can be evaluated twice — once to read, once to write. +/// +/// Container indexes are the only sub-expression that may carry side effects, so a non-trivial +/// index is evaluated once into a synthetic local and both evaluations read that local. The +/// rest of the receiver chain is composed exclusively of the shapes `static_place_type` +/// resolves, which are side-effect-free local, property, and element reads. +/// +/// Infallible by construction: the caller only reaches this for an argument +/// `static_place_type` already resolved, and that resolver matches exactly the variants below. +/// An unmatched shape is returned unchanged, which is the conservative identity — it cannot be +/// reached without emitting IR for a place this module then refuses to write back. +fn stabilize_place(ctx: &mut LoweringContext<'_, '_>, place: &Expr) -> Expr { + match &place.kind { + ExprKind::PropertyAccess { object, property } => { + let object = stabilize_place(ctx, object); + Expr::new( + ExprKind::PropertyAccess { + object: Box::new(object), + property: property.clone(), + }, + place.span, + ) + } + ExprKind::ArrayAccess { array, index } => { + let array = stabilize_place(ctx, array); + let index = stabilize_index(ctx, index); + Expr::new( + ExprKind::ArrayAccess { + array: Box::new(array), + index: Box::new(index), + }, + place.span, + ) + } + _ => place.clone(), + } +} + +/// Evaluates a container index once when re-evaluating it could repeat a side effect. +/// +/// Literals and already-stored locals are re-read directly; anything else is lowered into a +/// synthetic local whose variable reference replaces the original index expression, so +/// `sort($m[next_index()])` calls `next_index()` exactly once like PHP. +fn stabilize_index(ctx: &mut LoweringContext<'_, '_>, index: &Expr) -> Expr { + if matches!( + index.kind, + ExprKind::Variable(_) + | ExprKind::This + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + ) { + return index.clone(); + } + let value = lower_expr(ctx, index); + let value_type = normalize_value_php_type(ctx.builder.value_php_type(value.value)); + let temp = ctx.declare_synthetic_php_local(value_type.clone()); + ctx.store_local(&temp, value, value_type, Some(index.span)); + Expr::new(ExprKind::Variable(temp), index.span) +} + +#[cfg(test)] +mod tests { + //! Purpose: + //! Unit coverage for the by-reference place rewrite's argument classification. + //! + //! Called from: + //! - `cargo test` through Rust's test harness. + //! + //! Key details: + //! - These assertions are pure predicates over AST shapes; the end-to-end behavior is + //! covered by `tests/codegen/arrays/` and `tests/codegen/objects/property_access/`. + + use super::*; + use crate::parser::ast::StaticReceiver; + use crate::span::Span; + + /// A plain local argument is never treated as a rewritable place: the backend's + /// local-slot write-back already stores the separated array back for it. + #[test] + fn plain_local_is_not_a_candidate_place_shape() { + let local = Expr::new(ExprKind::Variable("a".to_string()), Span::dummy()); + assert!(!is_candidate_place_shape(&local)); + } + + /// Property, static-property, and element arguments are the shapes the rewrite considers. + #[test] + fn property_and_element_shapes_are_candidate_places() { + let span = Span::dummy(); + let object = Expr::new(ExprKind::Variable("o".to_string()), span); + let property = Expr::new( + ExprKind::PropertyAccess { + object: Box::new(object.clone()), + property: "items".to_string(), + }, + span, + ); + let static_property = Expr::new( + ExprKind::StaticPropertyAccess { + receiver: StaticReceiver::Self_, + property: "items".to_string(), + }, + span, + ); + let element = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(object), + index: Box::new(Expr::new(ExprKind::IntLiteral(0), span)), + }, + span, + ); + assert!(is_candidate_place_shape(&property)); + assert!(is_candidate_place_shape(&static_property)); + assert!(is_candidate_place_shape(&element)); + } +} diff --git a/src/ir_lower/expr/static_array_callbacks.rs b/src/ir_lower/expr/static_array_callbacks.rs index 479732e5fd..aff389f7c2 100644 --- a/src/ir_lower/expr/static_array_callbacks.rs +++ b/src/ir_lower/expr/static_array_callbacks.rs @@ -192,7 +192,12 @@ pub(super) fn lower_static_callable_value_call( )) } StaticCallableBinding::Builtin(function_name) => { - let php_type = call_return_type(ctx, &function_name, &operands); + let php_type = static_callable_builtin_result_type( + ctx, + &function_name, + &operands, + expr.span, + ); Some(emit_builtin_call_value( ctx, &function_name, diff --git a/src/ir_lower/expr/unset.rs b/src/ir_lower/expr/unset.rs index 6e69943783..b38f154427 100644 --- a/src/ir_lower/expr/unset.rs +++ b/src/ir_lower/expr/unset.rs @@ -198,11 +198,17 @@ pub(super) fn unset_property_access_has_direct_lowering( ) -> bool { matches!( property_unset_action(ctx, object, property), - Some(UnsetPropertyAction::Magic | UnsetPropertyAction::Noop) + Some( + UnsetPropertyAction::Magic + | UnsetPropertyAction::Noop + | UnsetPropertyAction::ClearTyped + | UnsetPropertyAction::RemoveDynamic + ) ) } /// Lowers `unset($object->property)` for magic and no-op property targets. +/// Lowers `unset($object->property)` for magic, no-op, fixed-slot and dynamic property targets. pub(super) fn lower_unset_property_access( ctx: &mut LoweringContext<'_, '_>, object: &Expr, @@ -217,6 +223,20 @@ pub(super) fn lower_unset_property_access( Some(UnsetPropertyAction::Noop) => { lower_expr(ctx, object); } + // Both storage shapes share `Op::PropUnset`: the backend already resolves the + // receiver's property storage, so it picks the fixed-slot marker or the + // dynamic-hash removal from the same instruction. + Some(UnsetPropertyAction::ClearTyped | UnsetPropertyAction::RemoveDynamic) => { + let object = lower_expr(ctx, object); + let data = ctx.intern_string(property); + ctx.emit_void( + Op::PropUnset, + vec![object.value], + Some(Immediate::Data(data)), + Op::PropUnset.default_effects(), + Some(expr.span), + ); + } Some(UnsetPropertyAction::Fallback) | None => {} } } @@ -226,6 +246,13 @@ pub(super) enum UnsetPropertyAction { Fallback, Magic, Noop, + /// The property has a DECLARED type, so PHP's `unset()` leaves it uninitialized — + /// a state elephc's fixed property slots represent exactly. + ClearTyped, + /// The property lives in the receiver's dynamic-property hash (`stdClass`, or an + /// undeclared name on an `#[AllowDynamicProperties]` class), where PHP's `unset()` + /// really is a key removal. + RemoveDynamic, } /// Selects the PHP-visible `unset()` behavior for a statically known object property operand. @@ -235,14 +262,26 @@ pub(super) fn property_unset_action( property: &str, ) -> Option { let (class_name, _) = isset_object_expr_class(ctx, object)?; + // Every `stdClass` property is a hash entry, so `unset()` is a plain key removal and + // `stdClass` declares no magic methods that could intercept it. if is_builtin_stdclass_name(&class_name) { - return Some(UnsetPropertyAction::Fallback); + return Some(UnsetPropertyAction::RemoveDynamic); } let class_info = ctx.classes.get(class_name.as_str())?; - if class_info.allow_dynamic_properties { - return Some(UnsetPropertyAction::Fallback); + if class_info.allow_dynamic_properties && class_info.visible_property(property).is_none() { + return Some(dynamic_property_unset_action(ctx, &class_name)); } if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + // PHP does NOT consult `__unset` for a property it can see: it removes the + // property itself. A DECLARED (typed) property becomes uninitialized, which + // elephc's fixed slots can represent exactly. + if class_info.visible_property_is_declared(property) { + return Some(UnsetPropertyAction::ClearTyped); + } + // An UNTYPED fixed slot has no "removed" state and no null-capable storage: + // PHP's later read must warn and answer `null`, which a slot the checker typed + // `Int`/`Str`/... cannot represent. Keep the explicit unsupported diagnostic + // rather than leaving a stale value or a garbage payload behind. return Some(UnsetPropertyAction::Fallback); } if class_method_signature(ctx, &class_name, &php_symbol_key("__unset")).is_some() { diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 2b9d61ec9d..fdf797ae66 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -80,6 +80,7 @@ pub(crate) fn lower_main( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, "main".to_string(), constants, None, @@ -274,6 +275,7 @@ pub(crate) fn lower_user_function( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, name.to_string(), constants, None, @@ -374,6 +376,7 @@ pub(crate) fn lower_class_method( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, name.clone(), constants, Some(class_name.to_string()), @@ -439,6 +442,7 @@ pub(crate) fn lower_eval_aot_function( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, "main".to_string(), constants, None, @@ -544,6 +548,7 @@ pub(crate) fn lower_eval_aot_scope_function( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, "main".to_string(), constants, None, @@ -643,6 +648,7 @@ pub(crate) fn lower_property_init_thunk( &check_result.throw_access_sites, &check_result.builtin_call_types, &check_result.loop_storage_types, + &check_result.string_incdec_locals, function_name.clone(), constants, Some(class_name.to_string()), @@ -838,6 +844,7 @@ fn lower_closure_function_with_signature( parent.throw_access_sites, parent.builtin_call_types, parent.loop_storage_types, + parent.string_incdec_locals, loop_storage_scope, &parent.constants, parent.current_class.clone(), @@ -874,6 +881,7 @@ fn lower_body_into_function( throw_access_sites: &std::collections::HashMap, builtin_call_types: &std::collections::HashMap, loop_storage_types: &crate::types::LoopStorageTypes, + string_incdec_locals: &std::collections::HashSet<(String, String)>, loop_storage_scope: String, constants: &std::collections::HashMap, current_class: Option, @@ -919,6 +927,7 @@ fn lower_body_into_function( throw_access_sites, builtin_call_types, loop_storage_types, + string_incdec_locals, loop_storage_scope, constants, top_level_env, @@ -1440,13 +1449,30 @@ fn direct_closure_return_type( /// which would otherwise coerce a boxed Mixed argument to an integer on return. A /// `return $obj->prop` where `$obj` is a captured/parameter object of a known class adopts /// the property's declared type, so a `fn &() => $o->items` closure returns the array type -/// rather than the syntactic integer default. +/// rather than the syntactic integer default. An array literal built out of those same +/// variables resolves its element/value slots the same way (see +/// `direct_closure_return_array_element_type`). fn direct_closure_return_expr_type( expr: &crate::parser::ast::Expr, captures: &[(String, PhpType, bool)], params: &[(String, PhpType)], classes: &std::collections::HashMap, ) -> PhpType { + // An array literal returned directly is stamped with this inferred type and its elements + // are coerced into it by `lower_return_expr`, so its slots must be resolved against the + // closure signature instead of the syntactic integer default. + if let ExprKind::ArrayLiteral(items) = &expr.kind { + if !items.is_empty() { + return PhpType::Array(Box::new(direct_closure_return_array_element_type( + items, captures, params, classes, + ))); + } + } + if let ExprKind::ArrayLiteralAssoc(pairs) = &expr.kind { + if !pairs.is_empty() { + return direct_closure_return_assoc_literal_type(pairs, captures, params, classes); + } + } if let ExprKind::ScopedConstantAccess { receiver: crate::parser::ast::StaticReceiver::Named(class_name), name, @@ -1502,6 +1528,100 @@ fn direct_closure_return_expr_type( crate::types::checker::infer_expr_type_syntactic(expr) } +/// Returns the EIR storage element type for an indexed array literal returned directly +/// from a closure, resolving every item against the closure's captures and parameters. +/// +/// This mirrors `crate::ir_lower::expr::array_literal_type_for_ir`, which types the very +/// same literal while lowering the body from `LoweringContext::local_types`. The two must +/// agree: `lower_return_expr` feeds the inferred return element type back into +/// `lower_array_literal_with_expected_type`, so a slot typed `int` here casts a boxed +/// `Mixed` argument to an integer on the way into the array — `function (mixed $a, mixed $b) +/// { return [$a, $b]; }` called as `(1, "z")` produced `[1, 0]`. The syntactic fallback used +/// before this helper existed types every unrecognized item `int`, which also mis-stamped +/// `string`, `float`, `bool`, and `array` parameters. +fn direct_closure_return_array_element_type( + items: &[crate::parser::ast::Expr], + captures: &[(String, PhpType, bool)], + params: &[(String, PhpType)], + classes: &std::collections::HashMap, +) -> PhpType { + let mut elem_ty = PhpType::Never; + for item in items { + elem_ty = crate::ir_lower::expr::merge_ir_indexed_element_type( + elem_ty, + direct_closure_return_array_item_type(item, captures, params, classes), + ); + } + elem_ty +} + +/// Returns the EIR storage element type contributed by one indexed array-literal item. +/// +/// A spread contributes its source array's element type (widened to `Mixed` for an +/// empty/unknown source, since `Void`/`Never` has no array-element representation), matching +/// the `ExprKind::Spread` arm of `array_literal_element_type_for_ir`. +fn direct_closure_return_array_item_type( + item: &crate::parser::ast::Expr, + captures: &[(String, PhpType, bool)], + params: &[(String, PhpType)], + classes: &std::collections::HashMap, +) -> PhpType { + if let ExprKind::Spread(inner) = &item.kind { + let source = direct_closure_return_array_item_type(inner, captures, params, classes); + return match source.codegen_repr() { + PhpType::Array(elem) => match elem.codegen_repr() { + PhpType::Void | PhpType::Never => PhpType::Mixed, + other => other, + }, + _ => PhpType::Mixed, + }; + } + // `null` has no narrower storage than the boxed cell, exactly as the lowering-side + // `ExprKind::Null` arm decides. + if matches!(item.kind, ExprKind::Null) { + return PhpType::Mixed; + } + crate::ir_lower::expr::ir_array_storage_type(direct_closure_return_expr_type( + item, captures, params, classes, + )) +} + +/// Returns the EIR storage type for an associative array literal returned directly from a +/// closure, resolving each value against the closure's captures and parameters. +/// +/// Keys keep the syntactic rules (`normalized_array_key_type` / `merge_array_key_types`) +/// used by `assoc_array_literal_type_for_ir`; only the value slots need the signature, +/// since a `function (string $s) { return ['k' => $s]; }` value slot typed `int` made the +/// caller read the string payload back as a raw integer. +fn direct_closure_return_assoc_literal_type( + pairs: &[(crate::parser::ast::Expr, crate::parser::ast::Expr)], + captures: &[(String, PhpType, bool)], + params: &[(String, PhpType)], + classes: &std::collections::HashMap, +) -> PhpType { + let mut key_ty = PhpType::Never; + let mut value_ty = PhpType::Never; + for (key, value) in pairs { + let next_key = crate::types::normalized_array_key_type( + key, + crate::types::checker::infer_expr_type_syntactic(key), + ); + key_ty = if matches!(key_ty, PhpType::Never) { + next_key + } else { + crate::types::merge_array_key_types(key_ty, next_key) + }; + value_ty = crate::ir_lower::expr::merge_ir_assoc_value_type( + value_ty, + direct_closure_return_array_item_type(value, captures, params, classes), + ); + } + PhpType::AssocArray { + key: Box::new(key_ty), + value: Box::new(value_ty), + } +} + /// Returns true when a statement list contains a `return ` for its own function body. fn body_contains_value_return(statements: &[Stmt]) -> bool { statements.iter().any(stmt_contains_value_return) diff --git a/src/ir_lower/mod.rs b/src/ir_lower/mod.rs index 7b5874b654..0548308a18 100644 --- a/src/ir_lower/mod.rs +++ b/src/ir_lower/mod.rs @@ -10,6 +10,7 @@ //! source order and emitting high-level EIR operations. //! - EIR is the only production backend; unsupported lowering must fail explicitly. +mod array_pointer_scan; mod builtin_datetime; mod context; mod effect_refinement; diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 28beca0e98..1b2326a10a 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -97,9 +97,32 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { }); } +/// Declares the hidden internal-array-pointer cursor slots used inside a loop body before +/// that body is lowered. +/// +/// Only loops need this. Everywhere else lowering order matches execution order, so a +/// store lowered before the variable's first pointer call also runs before it and the +/// entry-block seed of `0` is already the right cursor. A loop body re-executes, so its +/// assignments must be able to rewind a cursor whose first call is lowered later; the +/// rewind lives in `LoweringContext::store_local` and only fires once the slot exists. +fn predeclare_loop_array_pointer_cursors(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { + let body = match &stmt.kind { + StmtKind::While { body, .. } + | StmtKind::DoWhile { body, .. } + | StmtKind::For { body, .. } + | StmtKind::Foreach { body, .. } => body, + _ => return, + }; + crate::ir_lower::array_pointer_scan::predeclare_loop_cursors(ctx, body); +} + /// Lowers one statement exactly once against the current local representations. +/// +/// The terminated-block guard that used to live here now sits in `lower_stmt`, above the +/// representation fixpoint that drives this function. fn lower_stmt_once(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { lower_statement_concat_reset(ctx, stmt.span); + predeclare_loop_array_pointer_cursors(ctx, stmt); match &stmt.kind { StmtKind::Echo(expr) => lower_echo(ctx, expr, stmt.span), StmtKind::Assign { name, value } => lower_assign(ctx, name, value, stmt.span), diff --git a/src/ir_lower/stmt/typed_foreach.rs b/src/ir_lower/stmt/typed_foreach.rs index dffe6c1288..df32aaba86 100644 --- a/src/ir_lower/stmt/typed_foreach.rs +++ b/src/ir_lower/stmt/typed_foreach.rs @@ -97,8 +97,12 @@ pub(super) fn lower_foreach( // Apply the checker-computed loop header contract before lowering the source expression so // an iterated-and-mutated array is loaded with its stable payload representation. apply_loop_storage_contracts(ctx, loop_span, Some(array.span)); - let (source, source_is_borrowed_element) = - lower_foreach_source(ctx, array, value_by_ref); + let (source, source_is_borrowed_element) = lower_foreach_source(ctx, array, value_by_ref); + // Orthogonal to the borrowed-element pin taken after `IterStart` below: that one keeps a + // by-reference hash element's storage alive, this one takes the loop's reference on an + // object source. A borrowed element is never an object, so `retain_object_foreach_source` + // returns it untouched and `source_is_borrowed_element` still describes `source`. + let source = retain_object_foreach_source(ctx, source, array.span); let source_php_ty = ctx.builder.value_php_type(source.value); let source_ty = source_php_ty.codegen_repr(); let key_needs_null_init = key_var.is_some_and(|name| !ctx.local_slots.contains_key(name)); @@ -359,3 +363,43 @@ pub(super) fn initialize_foreach_mixed_local_if_needed( let boxed = ctx.box_value_as_mixed(null, PhpType::Mixed, Some(span)); ctx.store_foreach_initializer_local_only(name, boxed, PhpType::Mixed, Some(span)); } + +/// Takes the loop's own reference on an object `foreach` source. +/// +/// Iterating an object — a user `Iterator`/`IteratorAggregate`, or a `Generator` — +/// must keep it alive for the whole loop even when the body drops every other +/// owner (`foreach ($it as $v) { unset($it); }`), so the loop needs a reference of +/// its own. `Op::IterStart` used to take that reference with a bare backend +/// `incref` that nothing ever balanced, leaking the object and everything it owned +/// once per loop. It is taken here instead, as an `Op::Acquire` whose result is an +/// owning temporary: the loop's exit block and its `LoopCleanup` (early `return`, +/// multi-level `break`) already release such a value exactly once. +/// +/// The reference the *lowered source expression* carried is dropped right away +/// under the pre-existing "owning temporary" rule, so a fresh +/// `foreach (make_iter() as $v)` temporary is still released exactly once — just +/// before the loop rather than after it, which the acquire above makes safe. +/// +/// Non-object sources are returned untouched: the iterator aliases an array or +/// hash source, so retaining one would change its refcount and therefore its +/// copy-on-write behaviour inside the loop body. +fn retain_object_foreach_source( + ctx: &mut LoweringContext<'_, '_>, + source: LoweredValue, + span: Span, +) -> LoweredValue { + if !matches!( + ctx.builder.value_php_type(source.value).codegen_repr(), + PhpType::Object(_) + ) { + return source; + } + let retained = crate::ir_lower::ownership::acquire_if_refcounted(ctx, source, Some(span)); + if retained.value == source.value { + return source; + } + if ctx.value_is_owning_temporary(source) { + crate::ir_lower::ownership::release_if_owned(ctx, source, Some(span)); + } + retained +} diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index fc21a6247d..9fc7aae174 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -140,6 +140,7 @@ fn dummy_check_result() -> CheckResult { throw_access_sites: HashMap::new(), builtin_call_types: HashMap::new(), loop_storage_types: HashMap::new(), + string_incdec_locals: Default::default(), } } diff --git a/src/ir_lower/tests/mod.rs b/src/ir_lower/tests/mod.rs index 35ed8f51d2..20388e9095 100644 --- a/src/ir_lower/tests/mod.rs +++ b/src/ir_lower/tests/mod.rs @@ -68,6 +68,10 @@ fn lower_source_at(source: &str, main_file_path: &Path, parent: &Path) -> crate: &defines, ) .expect("autoload failed"); + // Mirrors `pipeline::compile`, which desugars the `func_get_args()` family between + // `autoload::run` and constant folding. Without it an example using those functions + // reaches the checker as an undefined call, so the corpus would fail on valid PHP. + let ast = crate::func_args::desugar(ast).expect("func_args desugar failed"); let ast = crate::optimize::fold_constants(ast); let check_result = crate::types::check_with_target(&ast, target).expect("type check failed"); let ast = crate::optimize::propagate_constants(ast); diff --git a/src/ir_passes/clobber.rs b/src/ir_passes/clobber.rs index c8de6e5d7b..c7d6ce46f6 100644 --- a/src/ir_passes/clobber.rs +++ b/src/ir_passes/clobber.rs @@ -34,17 +34,21 @@ pub(super) fn op_is_volatile_safe(op: Op) -> bool { op, // Constants materialized directly into the result register. ConstI64 | ConstBool | ConstNull | ConstF64 - // Integer arithmetic, bitwise, and shift: result + secondary/tertiary - // scratch only (see `lower_inst::arithmetic`). + // Integer arithmetic and bitwise: result + secondary/tertiary scratch only + // (see `lower_inst::arithmetic`). `IShl`, `IShrA`, `ISMod`, and `IDiv` are + // deliberately absent: their PHP guards (negative shift count, zero divisor, + // `PHP_INT_MIN % -1`) branch into `lower_inst::exceptions`, which allocates a + // throwable and calls into the runtime unwinder. `FToI` is absent for the same + // reason — every PHP float->int now routes through `__rt_php_float_to_int`. | IAdd | ISub | IMul | INeg | IBitAnd | IBitOr | IBitXor | IBitNot - | IShl | IShrA | ISMod | IDiv // Floating-point arithmetic: d0/d1 or xmm0/xmm1 only (`lower_inst::floats`). - | FAdd | FSub | FMul | FDiv | FNeg + // `FDiv` is excluded: its zero-divisor guard branches into an exception throw. + | FAdd | FSub | FMul | FNeg // Integer/float comparisons: result + secondary scratch only. | ICmp | FCmp - // Scalar int<->float conversions: inline scvtf/fcvtzs / cvtsi2sd/cvttsd2si. - | IToF | FToI + // Int-to-float promotion is still a single inline scvtf / cvtsi2sd. + | IToF | Nop ) } diff --git a/src/ir_passes/dead_store.rs b/src/ir_passes/dead_store.rs index 76dffbfd32..96ead01232 100644 --- a/src/ir_passes/dead_store.rs +++ b/src/ir_passes/dead_store.rs @@ -204,7 +204,7 @@ fn op_is_value_only_consumer(op: Op) -> bool { matches!( op, // Integer/float arithmetic and bitwise operators. - IAdd | ISub | IMul | ICheckedAdd | ICheckedSub | ICheckedMul | IDiv | ISDiv | ISMod | IPow | INeg | IBitAnd | IBitOr | IBitXor + IAdd | ISub | IMul | ICheckedAdd | ICheckedSub | ICheckedMul | ICheckedPow | IDiv | ISDiv | ISMod | IPow | INeg | IBitAnd | IBitOr | IBitXor | IBitNot | IShl | IShrA | FAdd | FSub | FMul | FDiv | FPow | FNeg | MixedNumericBinop // Comparisons. | ICmp | FCmp | StrEq | StrCmp | StrLooseEq | StrictEq | StrictNotEq | LooseEq diff --git a/src/lexer/literals/identifiers.rs b/src/lexer/literals/identifiers.rs index 0d522a6006..2d6bd6b067 100644 --- a/src/lexer/literals/identifiers.rs +++ b/src/lexer/literals/identifiers.rs @@ -176,6 +176,12 @@ pub(in crate::lexer) fn scan_keyword(cursor: &mut Cursor) -> Result Ok(Token::Global), "declare" => Ok(Token::Declare), "enddeclare" => Ok(Token::EndDeclare), + "endif" => Ok(Token::EndIf), + "endwhile" => Ok(Token::EndWhile), + "endfor" => Ok(Token::EndFor), + "endforeach" => Ok(Token::EndForeach), + "endswitch" => Ok(Token::EndSwitch), + "goto" => Ok(Token::Goto), "static" => Ok(Token::Static), "self" => Ok(Token::Self_), "trait" => Ok(Token::Trait), diff --git a/src/lexer/scan.rs b/src/lexer/scan.rs index 0b97342c77..d2486d553d 100644 --- a/src/lexer/scan.rs +++ b/src/lexer/scan.rs @@ -247,6 +247,7 @@ fn scan_token(cursor: &mut Cursor) -> Result { if cursor.peek() == Some('>') { cursor.advance(); Ok(Token::Spaceship) } else { Ok(Token::LessEqual) } } + else if cursor.peek() == Some('>') { cursor.advance(); Ok(Token::LessGreater) } else { Ok(Token::Less) } } '>' => { diff --git a/src/lexer/token.rs b/src/lexer/token.rs index c9a831de1d..97dd79efbe 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -136,6 +136,12 @@ pub enum Token { Global, // global Declare, // declare (strict_types/ticks/encoding directive) EndDeclare, // enddeclare (alternative declare block terminator) + EndIf, // endif (alternative if block terminator) + EndWhile, // endwhile (alternative while block terminator) + EndFor, // endfor (alternative for block terminator) + EndForeach, // endforeach (alternative foreach block terminator) + EndSwitch, // endswitch (alternative switch block terminator) + Goto, // goto (unconditional jump to a labeled statement) Static, // static Self_, // self Trait, // trait @@ -214,6 +220,7 @@ pub enum Token { EqualEqual, // == EqualEqualEqual, // === NotEqual, // != + LessGreater, // <> (PHP alias for !=) NotEqualEqual, // !== Less, // < Greater, // > @@ -309,6 +316,12 @@ impl Token { Token::Global => Some("global"), Token::Declare => Some("declare"), Token::EndDeclare => Some("enddeclare"), + Token::EndIf => Some("endif"), + Token::EndWhile => Some("endwhile"), + Token::EndFor => Some("endfor"), + Token::EndForeach => Some("endforeach"), + Token::EndSwitch => Some("endswitch"), + Token::Goto => Some("goto"), Token::Static => Some("static"), Token::Self_ => Some("self"), Token::Trait => Some("trait"), diff --git a/src/lib.rs b/src/lib.rs index be385745e3..a596215f6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,8 @@ pub mod errors; mod eval_aot; /// `#[Export]` attribute scan for cdylib emission. pub mod exports; +/// PHP variadic-argument introspection (`func_num_args`/`func_get_args`/`func_get_arg`) desugaring. +pub mod func_args; mod progress; /// Image (GD/Exif/Imagick/Gmagick/Cairo) standard-library prelude injection. pub mod hash_prelude; diff --git a/src/linker/command.rs b/src/linker/command.rs index c99e0e5df7..d9348a3d26 100644 --- a/src/linker/command.rs +++ b/src/linker/command.rs @@ -17,6 +17,25 @@ use crate::codegen::platform::{Platform, Target}; use crate::codegen::Emit; use crate::link_plan::{LinkItem, LinkOrigin, LinkPlan, LinuxLinkMode}; +/// ELF hardening options applied to every Linux output, in driver (`-Wl,`) form +/// so they reach `ld` verbatim for both the GCC and Clang drivers. +/// +/// - `noexecstack`: elephc assembles its objects with `as`, which never emits a +/// `.note.GNU-stack` section, so GNU ld infers an **executable** stack +/// (`PT_GNU_STACK` `RWE`) and warns that the inference is deprecated. Nothing +/// elephc produces needs it: there is no JIT, and fiber stacks are `mmap`ed +/// `PROT_READ|PROT_WRITE` with a `PROT_NONE` guard page +/// (`codegen_support::runtime::fibers::alloc`). +/// - `relro` + `now`: resolve relocations eagerly and remap the relocated head +/// of the data segment read-only. This also covers the `-static-pie` output +/// that default-PIE drivers already produce from `-static`, where the RELRO +/// segment holds the self-relocated GOT. +/// +/// None of the three can fail a link: `ld` accepts all of them for static, +/// dynamic, and `-shared` outputs, and silently ignores the ones that do not +/// apply to a given output kind. +const LINUX_HARDENING_FLAGS: [&str; 3] = ["-Wl,-z,noexecstack", "-Wl,-z,relro", "-Wl,-z,now"]; + /// Paths for the final output and its two required input objects. pub(super) struct LinkPaths<'a> { /// Final executable or shared-library path. @@ -178,6 +197,7 @@ fn render_linux_command( Emit::Executable => args.push(OsString::from("-Wl,--gc-sections")), Emit::Cdylib => args.push(OsString::from("-shared")), } + args.extend(LINUX_HARDENING_FLAGS.iter().copied().map(OsString::from)); args.extend([ OsString::from("-o"), paths.bin.as_os_str().to_owned(), @@ -322,6 +342,20 @@ mod tests { .arguments_lossy() } + /// Renders one Linux shared-library command with no host probes. + fn render_linux_cdylib(plan: &LinkPlan) -> Vec { + render_link_command( + Target::new(Platform::Linux, Arch::X86_64), + Emit::Cdylib, + paths(), + plan, + false, + None, + &[], + ) + .arguments_lossy() + } + /// Renders one macOS executable command with injected SDK and Homebrew paths. fn render_macos(plan: &LinkPlan) -> Vec { render_link_command( @@ -439,6 +473,59 @@ mod tests { assert!(args.contains(&"/cache/libelephc_pdo.a".to_string())); } + /// Verifies every Linux output carries the ELF hardening options, on both + /// supported architectures, in static and dynamic mode, and for shared + /// libraries. Without `-z noexecstack` the assembler objects (which have no + /// `.note.GNU-stack`) make GNU ld mark the stack `RWE`. + #[test] + fn linux_outputs_carry_elf_hardening_flags() { + let static_plan = LinkPlan::from_items(vec![LinkItem::managed_archive("pcre2.a", "pcre2")]); + let dynamic_plan = LinkPlan::from_items(vec![LinkItem::named_user("sqlite3")]); + let aarch64 = render_link_command( + Target::new(Platform::Linux, Arch::AArch64), + Emit::Executable, + paths(), + &static_plan, + false, + None, + &[], + ) + .arguments_lossy(); + + let commands = [ + render_linux(&static_plan), + render_linux(&dynamic_plan), + render_linux_cdylib(&dynamic_plan), + aarch64, + ]; + for args in commands { + for flag in LINUX_HARDENING_FLAGS { + assert!( + args.contains(&flag.to_string()), + "missing {flag} in Linux link command: {args:?}" + ); + } + } + } + + /// Verifies macOS link commands stay free of the ELF-only hardening options: + /// `ld64` rejects `-z` entirely, and macOS binaries are already PIE with a + /// platform-enforced non-executable stack. + #[test] + fn macos_command_omits_elf_hardening_flags() { + let args = render_macos(&LinkPlan::from_items(vec![LinkItem::named_extern("pcre2-8")])); + for flag in LINUX_HARDENING_FLAGS { + assert!( + !args.contains(&flag.to_string()), + "macOS link command must not carry {flag}: {args:?}" + ); + } + assert!( + !args.iter().any(|argument| argument.contains("-z")), + "macOS link command must not carry any -z option: {args:?}" + ); + } + /// Verifies the test fixture uses ordinary path values accepted by all hosts. #[test] fn renderer_fixture_paths_are_stable() { diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index 42f631c4b0..1b6116a2ba 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -31,6 +31,7 @@ pub(in crate::magic_constants) fn walk_program(stmts: Vec, pass: pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { let span = stmt.span; let source_mode = stmt.source_mode; + let strict_types = stmt.strict_types; let attributes = stmt.attributes.clone(); let kind = match stmt.kind { StmtKind::Synthetic(stmts) => StmtKind::Synthetic(walk_program(stmts, pass)), @@ -419,6 +420,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { kind, span, source_mode, + strict_types, attributes, } } diff --git a/src/main.rs b/src/main.rs index 7bf204ca5d..9ed08a8ef9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod conditional; mod errors; mod eval_aot; mod exports; +mod func_args; mod hash_prelude; mod image_prelude; mod intrinsics; diff --git a/src/name_resolver/names.rs b/src/name_resolver/names.rs index 7319ea9cf4..2a98268dd0 100644 --- a/src/name_resolver/names.rs +++ b/src/name_resolver/names.rs @@ -7,6 +7,10 @@ //! //! Key details: //! - PHP class-like names are resolved differently from function and constant fallback lookups. +//! - The leading segment of a *qualified* name (`M\thing`) is expanded through the +//! class/namespace import table for classes, functions, and constants alike +//! (`expand_qualified_namespace_alias`); `use function` / `use const` aliases apply only to +//! unqualified names, and fully-qualified names are never expanded. use crate::errors::CompileError; use crate::names::{php_symbol_key, Name}; @@ -122,6 +126,31 @@ pub(super) fn register_imports( Ok(()) } +/// Expands the leading segment of a *qualified* name (contains `\`, no leading `\`) +/// through the class/namespace import table. +/// +/// PHP translates the first segment of a qualified name using the class/namespace import +/// table (plain `use X as A;`) regardless of whether the name ultimately denotes a class, +/// a function, or a constant. `use function` / `use const` aliases apply only to +/// *unqualified* names, and fully-qualified names (`\A\b`) are never expanded. Alias +/// lookup is case-insensitive, and only the first segment is ever substituted. +/// +/// Returns `None` when the name is unqualified or fully qualified, or when its first +/// segment is not a registered alias. +pub(super) fn expand_qualified_namespace_alias(name: &Name, imports: &Imports) -> Option { + if name.is_fully_qualified() || name.is_unqualified() { + return None; + } + let first = name.parts.first()?; + let alias = imports.classes.get(&php_symbol_key(first))?; + let suffix = &name.parts[1..]; + if suffix.is_empty() { + Some(alias.clone()) + } else { + Some(format!("{}\\{}", alias, suffix.join("\\"))) + } +} + /// Resolves "self", "parent", "static" to their lowercase special-name form; /// delegates to `resolved_class_name` for all other names. pub(super) fn resolve_special_or_class_name( @@ -162,18 +191,10 @@ pub(super) fn resolved_class_name( .canonical_class_like(alias) .unwrap_or_else(|| alias.clone()); } - } else if let Some(first) = name.parts.first() { - if let Some(alias) = imports.classes.get(&php_symbol_key(first)) { - let suffix = &name.parts[1..]; - let candidate = if suffix.is_empty() { - alias.clone() - } else { - format!("{}\\{}", alias, suffix.join("\\")) - }; - return symbols - .canonical_class_like(&candidate) - .unwrap_or(candidate); - } + } else if let Some(candidate) = expand_qualified_namespace_alias(name, imports) { + return symbols + .canonical_class_like(&candidate) + .unwrap_or(candidate); } let candidate = if let Some(namespace) = current_namespace { if !namespace.is_empty() { @@ -204,14 +225,8 @@ pub(super) fn resolved_class_constant_name( { return alias.clone(); } - } else if let Some(first) = name.parts.first() { - if let Some(alias) = imports.classes.get(&php_symbol_key(first)) { - let suffix = &name.parts[1..]; - if suffix.is_empty() { - return alias.clone(); - } - return format!("{}\\{}", alias, suffix.join("\\")); - } + } else if let Some(candidate) = expand_qualified_namespace_alias(name, imports) { + return candidate; } if let Some(namespace) = current_namespace { if !namespace.is_empty() { @@ -264,18 +279,8 @@ pub(super) fn resolve_function_name( } return local; } - if let Some(first) = name.parts.first() { - if let Some(alias) = imports.functions.get(&php_symbol_key(first)) { - let suffix = &name.parts[1..]; - let candidate = if suffix.is_empty() { - alias.clone() - } else { - format!("{}\\{}", alias, suffix.join("\\")) - }; - return symbols - .canonical_function(&candidate) - .unwrap_or(candidate); - } + if let Some(candidate) = expand_qualified_namespace_alias(name, imports) { + return symbols.canonical_function(&candidate).unwrap_or(candidate); } let candidate = if let Some(namespace) = current_namespace { if !namespace.is_empty() { @@ -330,14 +335,8 @@ pub(super) fn resolve_constant_name( } return local; } - if let Some(first) = name.parts.first() { - if let Some(alias) = imports.constants.get(first) { - let suffix = &name.parts[1..]; - if suffix.is_empty() { - return alias.clone(); - } - return format!("{}\\{}", alias, suffix.join("\\")); - } + if let Some(candidate) = expand_qualified_namespace_alias(name, imports) { + return candidate; } if let Some(namespace) = current_namespace { if !namespace.is_empty() { @@ -383,6 +382,9 @@ fn is_builtin_global_constant(name: &str) -> bool { | "ARRAY_FILTER_USE_VALUE" | "ARRAY_FILTER_USE_BOTH" | "ARRAY_FILTER_USE_KEY" + | "STR_PAD_LEFT" + | "STR_PAD_RIGHT" + | "STR_PAD_BOTH" | "STDIN" | "STDOUT" | "STDERR" @@ -405,11 +407,13 @@ fn is_builtin_global_constant(name: &str) -> bool { ) { return true; } - // Shared source-of-truth slices for JSON, stream/socket, and session constants. + // Shared source-of-truth slices for JSON, stream/socket, session, array, and math constants. crate::types::json_constants::JSON_INT_CONSTANTS .iter() .chain(crate::types::stream_constants::STREAM_INT_CONSTANTS.iter()) .chain(crate::types::session_constants::SESSION_INT_CONSTANTS.iter()) .chain(crate::types::error_constants::ERROR_LEVEL_CONSTANTS.iter()) + .chain(crate::types::array_constants::ARRAY_INT_CONSTANTS.iter()) + .chain(crate::types::math_constants::MATH_INT_CONSTANTS.iter()) .any(|(constant_name, _)| *constant_name == name) } diff --git a/src/name_resolver/statements/list.rs b/src/name_resolver/statements/list.rs index 6440b08490..98a49db323 100644 --- a/src/name_resolver/statements/list.rs +++ b/src/name_resolver/statements/list.rs @@ -52,7 +52,7 @@ pub(in crate::name_resolver) fn resolve_stmt_list( register_imports(&mut imports, use_items, stmt.span)?; } _ => { - let resolved_decl = crate::source::with_parse_mode(stmt.source_mode, || { + let resolved_decl = crate::source::with_parse_mode(stmt.profile(), || { crate::strict_php::with_source_mode(stmt.source_mode, || { resolve_decl_stmt(stmt, namespace.as_deref(), &imports, symbols) }) @@ -63,7 +63,7 @@ pub(in crate::name_resolver) fn resolve_stmt_list( } let ctx = ResolveContext::new(namespace.as_deref(), &imports, symbols); - let resolved_stmt = crate::source::with_parse_mode(stmt.source_mode, || { + let resolved_stmt = crate::source::with_parse_mode(stmt.profile(), || { crate::strict_php::with_source_mode(stmt.source_mode, || { resolve_regular_stmt(stmt, ctx) }) diff --git a/src/names.rs b/src/names.rs index 8188237928..cde3c30241 100644 --- a/src/names.rs +++ b/src/names.rs @@ -7,6 +7,10 @@ //! //! Key details: //! - PHP symbol lookup and emitted assembly labels depend on these transformations staying stable. +//! - Composite symbols (class + member, function + static local, …) must be built with +//! `join_symbol_fragments()`/`join_php_symbol()`. Joining mangled fragments with a bare `_` +//! is ambiguous because mangled fragments contain `_`, which silently merged unrelated PHP +//! declarations onto one storage cell. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// Kind of PHP name based on how it was written in source. @@ -214,6 +218,83 @@ pub fn mangle_fqn(name: &str) -> String { mangled } +/// Separator inserted between a symbol prefix and mangled fragments when at least one +/// fragment carries a `mangle_fqn()` escape. +/// +/// A `mangle_fqn()` result is a concatenation of single alphanumerics and the escape groups +/// `_u_`, `_N_` and `_xNN_`. Every escape opens and closes with exactly one `_` and has a +/// non-empty body, so the longest run of consecutive underscores a mangled fragment can +/// contain is two (the closing `_` of one escape followed by the opening `_` of the next), +/// and a mangled fragment can neither start nor end with `__`. Three underscores therefore +/// never occur inside a mangled fragment, which makes them usable as a boundary marker. +const ESCAPED_FRAGMENT_SEPARATOR: &str = "___"; + +/// Separator inserted between a symbol prefix and mangled fragments when every fragment is a +/// plain alphanumeric run. Keeps the common `_method_Foo_bar` symbol shape readable. +const COMPACT_FRAGMENT_SEPARATOR: &str = "_"; + +/// Returns `true` when a mangled fragment is a non-empty run of ASCII alphanumerics. +/// +/// Such fragments contain no `_` at all, so a single-underscore separator between them is +/// unambiguous. Any fragment that went through a `mangle_fqn()` escape fails this test. +fn is_compact_fragment(fragment: &str) -> bool { + !fragment.is_empty() && fragment.bytes().all(|byte| byte.is_ascii_alphanumeric()) +} + +/// Joins a fixed symbol prefix with already-mangled fragments so that the result is injective +/// in the fragment tuple. +/// +/// `prefix` must be a compile-time literal without a trailing separator (e.g. `"_method"`); +/// `fragments` must be `mangle_fqn()` results or decimal numbers. Two separator regimes are +/// used and they can never be confused with one another: +/// +/// - every fragment alphanumeric → `prefix_f1_f2`. The joined tail then contains exactly one +/// `_` per fragment and no run of two, so splitting on `_` recovers the tuple. +/// - otherwise → `prefix___f1___f2`. Fragments contain no `___`, so the runs of three or more +/// underscores mark exactly the boundaries. A boundary run has length 3 to 5 (a fragment +/// contributes at most one adjacent `_`), and the split is still unique because no +/// `mangle_fqn()` result stays valid when a trailing `_` is added or removed: an escape's +/// closing `_` cannot be dropped and a dangling `_` cannot be appended. +/// +/// The two regimes are distinguished by the run length alone (the compact form never contains +/// two adjacent underscores in the joined tail, the escaped form always contains at least three). +pub fn join_symbol_fragments(prefix: &str, fragments: &[&str]) -> String { + let separator = if fragments.iter().all(|fragment| is_compact_fragment(fragment)) { + COMPACT_FRAGMENT_SEPARATOR + } else { + ESCAPED_FRAGMENT_SEPARATOR + }; + let mut symbol = String::from(prefix); + for fragment in fragments { + symbol.push_str(separator); + symbol.push_str(fragment); + } + symbol +} + +/// Mangles each raw PHP name and joins them onto `prefix` with `join_symbol_fragments()`. +/// +/// This is the only supported way to build a symbol or label out of more than one PHP name. +pub fn join_php_symbol(prefix: &str, names: &[&str]) -> String { + let mangled: Vec = names.iter().map(|name| mangle_fqn(name)).collect(); + let fragments: Vec<&str> = mangled.iter().map(String::as_str).collect(); + join_symbol_fragments(prefix, &fragments) +} + +/// Converts an arbitrary PHP-derived name into a decorative assembly-label fragment. +/// +/// Every non-alphanumeric byte collapses to `_`, so this is deliberately **not** injective: +/// `a_b` and `aéb` produce the same fragment. It may only be used for the human-readable part +/// of a label whose uniqueness is already guaranteed by a separate unique numeric id (see +/// `crate::codegen::context::FunctionContext::next_label()`). Any label that must be unique on +/// its own has to be built with `join_php_symbol()` instead. +pub fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + #[cfg(test)] mod mangle_tests { use super::*; @@ -246,6 +327,192 @@ mod mangle_tests { assert_ne!(mangle_fqn("价"), mangle_fqn("a")); assert_ne!(mangle_fqn("a_b"), mangle_fqn("a\\b")); } + + /// Verifies the documented separator invariant the joiner relies on: no `mangle_fqn()` + /// result ever contains three consecutive underscores, and none starts or ends with two. + #[test] + fn mangled_fragments_never_contain_the_escaped_separator() { + let adversarial = [ + "_", "__", "___", "____", "_S_", "_u_", "_N_", "_x5f_", "\\", "\\\\", "_\\_", + "a_\\_b", "__construct", "价_格", "_é_", "\\_\\_", + ]; + for name in adversarial { + let mangled = mangle_fqn(name); + assert!( + !mangled.contains(ESCAPED_FRAGMENT_SEPARATOR), + "mangle_fqn({name:?}) = {mangled:?} contains the fragment separator" + ); + assert!( + !mangled.starts_with("__") && !mangled.ends_with("__"), + "mangle_fqn({name:?}) = {mangled:?} must not start or end with two underscores" + ); + } + } + + /// Verifies the adversarial `_S_` case called out for naive separator schemes: the PHP name + /// `_S_` mangles to a string containing `_S_`, so `_S_` would be an unusable separator, + /// while the `___` separator survives it. + #[test] + fn joiner_survives_the_self_referential_separator_name() { + assert!(mangle_fqn("_S_").contains("_S_")); + assert_ne!( + join_php_symbol("_p", &["a", "_S_b"]), + join_php_symbol("_p", &["a_S", "b"]) + ); + assert_ne!( + join_php_symbol("_p", &["a", "___b"]), + join_php_symbol("_p", &["a___", "b"]) + ); + } + + /// Verifies the compact regime is used only for alphanumeric fragments and keeps the + /// historical readable symbol shape. + #[test] + fn joiner_keeps_alphanumeric_symbols_compact() { + assert_eq!(method_symbol("Exception", "run"), "_method_Exception_run"); + assert_eq!(static_method_symbol("Foo", "bar"), "_static_Foo_bar"); + assert_eq!(static_property_symbol("Foo", "bar"), "_static_prop_Foo_bar"); + assert_eq!(enum_case_symbol("Suit", "Hearts"), "_enum_case_Suit_Hearts"); + assert_eq!(static_local_symbol("f", "x"), "_static_local_f_x"); + assert_eq!(interface_method_wrapper_symbol(1, 2, "run"), "_ifacewrap_1_2_run"); + } + + /// Verifies the reported static-property collision (`a::$u_b` versus `a_u::$b`) now maps to + /// two distinct `.comm` symbols instead of merging both classes onto one storage cell. + #[test] + fn static_property_symbols_do_not_collide_on_underscore_boundaries() { + assert_ne!( + static_property_symbol("a", "u_b"), + static_property_symbol("a_u", "b") + ); + } + + /// Verifies the reported method / static-method / enum-case collisions, which previously + /// made valid PHP fail to assemble with a duplicate-symbol error. + #[test] + fn member_symbols_do_not_collide_on_underscore_boundaries() { + assert_ne!(method_symbol("a", "u_b"), method_symbol("a_u", "b")); + assert_ne!( + static_method_symbol("a", "u_b"), + static_method_symbol("a_u", "b") + ); + assert_ne!(enum_case_symbol("a", "u_b"), enum_case_symbol("a_u", "b")); + assert_ne!( + interface_method_wrapper_symbol(1, 2, "u_b"), + interface_method_wrapper_symbol(1, 2, "b") + ); + } + + /// Verifies the static-local storage and initialization-flag namespaces stay disjoint, so a + /// PHP static named `$x_init` can no longer alias the init flag of static `$x`. + #[test] + fn static_local_flag_symbols_cannot_be_spelled_by_a_php_variable() { + assert_ne!( + static_local_symbol("f", "x_init"), + static_local_init_symbol("f", "x") + ); + assert_ne!( + static_local_symbol("f", "x"), + static_local_init_symbol("f", "x") + ); + assert_ne!( + static_local_init_symbol("f", "x_init"), + static_local_init_symbol("f_init", "x") + ); + } + + /// Verifies static locals of two distinct functions never share one storage cell, including + /// the non-ASCII case where the old fragment helper collapsed `é` to `_`. + #[test] + fn static_local_symbols_do_not_collide_across_functions() { + assert_ne!( + static_local_symbol("a", "b_c"), + static_local_symbol("aéb", "c") + ); + assert_ne!( + static_local_symbol("a_b", "c"), + static_local_symbol("a", "b_c") + ); + assert_ne!( + static_local_symbol("A::m", "x"), + static_local_symbol("A", "m_x") + ); + } + + /// Verifies the distinct symbol kinds cannot spell one another, in particular the + /// static-method / static-local overlap that shared the `_static_` prefix and produced an + /// `invalid symbol redefinition` on a class and function with the same name. + #[test] + fn symbol_kinds_stay_in_disjoint_namespaces() { + assert_ne!(static_method_symbol("A", "m"), static_local_symbol("A", "m")); + assert_ne!( + static_method_symbol("prop", "x"), + static_property_symbol("prop", "x") + ); + assert_ne!( + static_method_symbol("local", "x"), + static_local_symbol("local", "x") + ); + assert_ne!( + format!("{}_epilogue", method_symbol("A", "m")), + method_symbol("A", "m_epilogue") + ); + assert_ne!( + format!("{}__genbody", method_symbol("A", "m")), + method_symbol("A", "m__genbody") + ); + } + + /// Verifies the joiner is injective over an exhaustive cross product of adversarial name + /// pairs, which is the property every composite symbol builder depends on. + #[test] + fn joiner_is_injective_over_adversarial_name_pairs() { + let names = [ + "a", "b", "a_", "_a", "a_b", "a__b", "a_u", "u_b", "_", "__", "___", "_S_", "_u_", + "_N_", "A\\b", "a\\b", "aéb", "a_é", "é", "x_init", "init", "prop", "local", "1", + "12", "价格", "a价", "_x5f_", "\\_", + ]; + let mut seen: std::collections::HashMap = + std::collections::HashMap::new(); + for left in names { + for right in names { + let symbol = join_php_symbol("_k", &[left, right]); + if let Some(previous) = seen.insert(symbol.clone(), (left, right)) { + panic!("{previous:?} and {:?} both produce {symbol:?}", (left, right)); + } + } + } + } + + /// Verifies three-fragment joins stay injective too, covering interface wrappers and the + /// eval-bridge class/declaring-class/member labels. + #[test] + fn joiner_is_injective_over_adversarial_name_triples() { + let names = ["a", "b", "a_b", "_", "_u_", "aéb", "a\\b", "1", "12"]; + let mut seen: std::collections::HashMap = + std::collections::HashMap::new(); + for first in names { + for second in names { + for third in names { + let symbol = join_php_symbol("_k", &[first, second, third]); + if let Some(previous) = seen.insert(symbol.clone(), (first, second, third)) { + panic!( + "{previous:?} and {:?} both produce {symbol:?}", + (first, second, third) + ); + } + } + } + } + } + + /// Verifies `label_fragment()` stays a pure decoration helper: it is documented as + /// non-injective, and this pins the collision so nobody grows a uniqueness assumption on it. + #[test] + fn label_fragment_is_decorative_and_not_injective() { + assert_eq!(label_fragment("a_b"), "a_b"); + assert_eq!(label_fragment("aéb"), label_fragment("a_b")); + } } /// Returns the global function symbol label for a given PHP function name. @@ -297,55 +564,66 @@ pub fn function_epilogue_symbol(name: &str) -> String { /// Returns the instance method symbol for a class/method pair. /// -/// Format: `_method__`. Used for virtual dispatch -/// and method table entries. +/// Format: `_method__` for alphanumeric names, `_method______` +/// once either name needs a `mangle_fqn()` escape. Used for virtual dispatch and method table +/// entries; the epilogue label appends `_epilogue` to this symbol. pub fn method_symbol(class_name: &str, method_name: &str) -> String { - format!( - "_method_{}_{}", - mangle_fqn(class_name), - mangle_fqn(method_name) - ) + join_php_symbol("_method", &[class_name, method_name]) } /// Returns the interface method wrapper symbol for a class/interface/method triplet. /// -/// Format: `_ifacewrap___`. Used by the -/// runtime to route interface method calls through concrete implementation wrappers. +/// Format: `_ifacewrap___`. Used by the runtime to route +/// interface method calls through concrete implementation wrappers. The two ids are decimal +/// numbers and join as plain fragments. pub fn interface_method_wrapper_symbol( class_id: u64, interface_id: u64, method_name: &str, ) -> String { - format!( - "_ifacewrap_{}_{}_{}", - class_id, - interface_id, - mangle_fqn(method_name) + let class_id = class_id.to_string(); + let interface_id = interface_id.to_string(); + join_symbol_fragments( + "_ifacewrap", + &[&class_id, &interface_id, &mangle_fqn(method_name)], ) } /// Returns the static method symbol for a class/method pair. /// -/// Format: `_static__`. Used for static method -/// dispatch and method table entries. +/// Format: `_static__`, escaping to `_static______` when either +/// name is not purely alphanumeric. Used for static method dispatch and method table entries. pub fn static_method_symbol(class_name: &str, method_name: &str) -> String { - format!( - "_static_{}_{}", - mangle_fqn(class_name), - mangle_fqn(method_name) - ) + join_php_symbol("_static", &[class_name, method_name]) } /// Returns the static property symbol for a class/property pair. /// -/// Format: `_static_prop__`. Used for static -/// property access and the property lookup table. +/// Format: `_static_prop__`, escaping to `_static_prop______` +/// when either name is not purely alphanumeric. Used for static property access and the +/// property lookup table. pub fn static_property_symbol(class_name: &str, property_name: &str) -> String { - format!( - "_static_prop_{}_{}", - mangle_fqn(class_name), - mangle_fqn(property_name) - ) + join_php_symbol("_static_prop", &[class_name, property_name]) +} + +/// Returns the storage symbol for one function-scoped `static $var` declaration. +/// +/// Format: `_static_local__`, escaping to +/// `_static_local______` when either name is not purely alphanumeric. +/// The dedicated `_static_local` prefix keeps this namespace disjoint from +/// `static_method_symbol()`, which used to share the `_static_` prefix and made a class's +/// static method collide with a same-named function's static local. +pub fn static_local_symbol(function_name: &str, variable_name: &str) -> String { + join_php_symbol("_static_local", &[function_name, variable_name]) +} + +/// Returns the one-shot initialization flag symbol paired with `static_local_symbol()`. +/// +/// Format: `_static_local_init__`. Derived from the same injective +/// (function, variable) encoding rather than by suffixing the storage symbol, so no PHP-legal +/// variable name can spell another static's flag symbol. +pub fn static_local_init_symbol(function_name: &str, variable_name: &str) -> String { + join_php_symbol("_static_local_init", &[function_name, variable_name]) } /// Returns the synthetic accessor-method name for a property's `get` hook. @@ -366,12 +644,9 @@ pub fn property_hook_set_method(property_name: &str) -> String { /// Returns the enum case symbol for an enum/case pair. /// -/// Format: `_enum_case__`. Used for enum case -/// lookup and the enum case table. +/// Format: `_enum_case__`, escaping to `_enum_case______` when either +/// name is not purely alphanumeric (namespaced enums always take the escaped form). Used for +/// enum case lookup and the enum case table. pub fn enum_case_symbol(enum_name: &str, case_name: &str) -> String { - format!( - "_enum_case_{}_{}", - mangle_fqn(enum_name), - mangle_fqn(case_name) - ) + join_php_symbol("_enum_case", &[enum_name, case_name]) } diff --git a/src/optimize.rs b/src/optimize.rs index 3b30727253..fe1431a05c 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -121,6 +121,52 @@ impl PropagatedValue { PropagatedValue::ArrayLit(_) => None, } } + + /// Returns whether two facts denote the *same constant*, i.e. whether merging control-flow + /// paths that carry them can substitute either one without changing program output. + /// + /// Stricter than `PartialEq` for floats: `0.0` and `-0.0` compare equal under IEEE but + /// `echo` prints `0` and `-0`, so a merge that unified them would change the program. + fn same_constant(&self, other: &Self) -> bool { + match (self, other) { + (PropagatedValue::Scalar(left), PropagatedValue::Scalar(right)) => { + left.same_constant(right) + } + (PropagatedValue::ArrayLit(left), PropagatedValue::ArrayLit(right)) => { + same_array_literal_fact(left, right) + } + _ => false, + } + } +} + +/// Returns whether two array-literal facts hold identical constants. +/// +/// `assigned_array_fact` only produces literals whose keys and values are scalar literals, so +/// the comparison walks them through `ScalarValue::same_constant` and keeps signed zeros apart. +/// Anything that is not one of those two literal shapes falls back to structural equality. +fn same_array_literal_fact(left: &Expr, right: &Expr) -> bool { + /// Compares two scalar-literal expressions by constant identity. + fn same_scalar(left: &Expr, right: &Expr) -> bool { + match (scalar_value(left), scalar_value(right)) { + (Some(left), Some(right)) => left.same_constant(&right), + _ => left == right, + } + } + + match (&left.kind, &right.kind) { + (ExprKind::ArrayLiteral(left), ExprKind::ArrayLiteral(right)) => { + left.len() == right.len() + && left.iter().zip(right).all(|(left, right)| same_scalar(left, right)) + } + (ExprKind::ArrayLiteralAssoc(left), ExprKind::ArrayLiteralAssoc(right)) => { + left.len() == right.len() + && left.iter().zip(right).all(|((left_key, left_value), (right_key, right_value))| { + same_scalar(left_key, right_key) && same_scalar(left_value, right_value) + }) + } + _ => left == right, + } } /// Maps local names to propagated facts during constant propagation. diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 9d6079a409..9ae902a0d2 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -233,7 +233,7 @@ fn guard_literal_to_scalar(value: &GuardLiteral) -> ScalarValue { GuardLiteral::Bool(value) => ScalarValue::Bool(*value), GuardLiteral::Null => ScalarValue::Null, GuardLiteral::Int(value) => ScalarValue::Int(*value), - GuardLiteral::Float(bits) => ScalarValue::Float(f64::from_bits(*bits)), + GuardLiteral::Float(value) => ScalarValue::Float(*value), GuardLiteral::String(value) => ScalarValue::String(value.clone()), } } @@ -256,7 +256,7 @@ fn known_subject_truthiness(subject: &Expr, guards: &GuardState) -> Option ScalarValue::Bool(value) => GuardLiteral::Bool(value), ScalarValue::Null => GuardLiteral::Null, ScalarValue::Int(value) => GuardLiteral::Int(value), - ScalarValue::Float(value) => GuardLiteral::Float(value.to_bits()), + ScalarValue::Float(value) => GuardLiteral::Float(value), ScalarValue::String(value) => GuardLiteral::String(value), }; return Some(guard_literal_truthy(&guard_literal)); @@ -319,8 +319,8 @@ pub(crate) fn dce_stmt(stmt: Stmt) -> Vec { /// side-effect-free expression statements. Guard state is propagated and invalidated /// based on writes and branch structure. fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { - let source_mode = stmt.source_mode; - crate::source::with_parse_mode(source_mode, || { + let profile = stmt.profile(); + crate::source::with_parse_mode(profile, || { dce_stmt_in_source_mode(stmt, guards) }) } @@ -329,11 +329,13 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { let span = stmt.span; let source_mode = stmt.source_mode; + let strict_types = stmt.strict_types; match stmt.kind { StmtKind::Echo(expr) => vec![Stmt { kind: StmtKind::Echo(prune_expr(expr)), span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::Assign { name, value } => vec![Stmt { @@ -343,12 +345,14 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::RefAssign { target, source } => vec![Stmt { kind: StmtKind::RefAssign { target, source }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::TypedAssign { @@ -363,6 +367,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::PropertyAssign { @@ -377,6 +382,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::StaticPropertyAssign { @@ -391,6 +397,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::StaticPropertyArrayPush { @@ -405,6 +412,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::StaticPropertyArrayAssign { @@ -421,6 +429,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::PropertyArrayAssign { @@ -437,6 +446,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::PropertyArrayPush { @@ -451,6 +461,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ArrayAssign { array, index, value } => vec![Stmt { @@ -461,6 +472,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::NestedArrayAssign { target, value } => vec![Stmt { @@ -470,6 +482,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ArrayPush { array, value } => vec![Stmt { @@ -479,6 +492,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ListUnpack { vars, value } => vec![Stmt { @@ -488,6 +502,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::StaticVar { name, init } => vec![Stmt { @@ -497,6 +512,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ConstDecl { name, value } => vec![Stmt { @@ -506,12 +522,14 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::IncludeOnceMark { label } => vec![Stmt { kind: StmtKind::IncludeOnceMark { label }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::IncludeOnceGuard { label, body } => vec![Stmt { @@ -521,6 +539,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::If { @@ -548,6 +567,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -563,6 +583,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -576,6 +597,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -606,6 +628,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -634,6 +657,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -654,6 +678,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::FunctionDecl { @@ -682,6 +707,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -689,12 +715,14 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { kind: StmtKind::Return(expr.map(prune_expr)), span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::Throw(expr) => vec![Stmt { kind: StmtKind::Throw(prune_expr(expr)), span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ClassDecl { @@ -729,6 +757,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -739,6 +768,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { kind: StmtKind::ExprStmt(expr), span, source_mode, + strict_types, attributes: Vec::new(), }] } else { @@ -765,12 +795,14 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::PackedClassDecl { name, fields } => vec![Stmt { kind: StmtKind::PackedClassDecl { name, fields }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::InterfaceDecl { @@ -792,6 +824,7 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::TraitDecl { @@ -813,8 +846,9 @@ fn dce_stmt_in_source_mode(stmt: Stmt, guards: &GuardState) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], - kind => vec![Stmt { kind, span, source_mode, attributes: Vec::new() }], + kind => vec![Stmt { kind, span, source_mode, strict_types, attributes: Vec::new() }], } } diff --git a/src/optimize/control/dce/guards/eval.rs b/src/optimize/control/dce/guards/eval.rs index ccf74a088d..903a79b865 100644 --- a/src/optimize/control/dce/guards/eval.rs +++ b/src/optimize/control/dce/guards/eval.rs @@ -38,7 +38,7 @@ pub(in crate::optimize::control::dce) fn scalar_guard_value(expr: &Expr) -> Opti ExprKind::BoolLiteral(value) => Some(GuardLiteral::Bool(*value)), ExprKind::Null => Some(GuardLiteral::Null), ExprKind::IntLiteral(value) => Some(GuardLiteral::Int(*value)), - ExprKind::FloatLiteral(value) => Some(GuardLiteral::Float(value.to_bits())), + ExprKind::FloatLiteral(value) => Some(GuardLiteral::Float(*value)), ExprKind::StringLiteral(value) => Some(GuardLiteral::String(value.clone())), _ => None, } @@ -79,7 +79,7 @@ pub(in crate::optimize::control::dce) fn guard_literal_truthy(value: &GuardLiter GuardLiteral::Bool(value) => *value, GuardLiteral::Null => false, GuardLiteral::Int(value) => *value != 0, - GuardLiteral::Float(bits) => f64::from_bits(*bits) != 0.0, + GuardLiteral::Float(value) => *value != 0.0, GuardLiteral::String(value) => !value.is_empty() && value != "0", } } diff --git a/src/optimize/control/dce/state.rs b/src/optimize/control/dce/state.rs index ffca19a7ce..a42e909ce4 100644 --- a/src/optimize/control/dce/state.rs +++ b/src/optimize/control/dce/state.rs @@ -92,9 +92,11 @@ impl GuardState { } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq)] /// Records an exact constraint that a variable holds a specific literal value. /// `name` is the variable name; `value` is the known literal. +/// +/// Not `Eq`: `GuardLiteral` equality is PHP's `===`, which is not reflexive for NAN. pub(super) struct ExactGuard { pub(super) name: String, pub(super) value: GuardLiteral, @@ -110,17 +112,36 @@ pub(super) struct ConditionGuard { pub(super) names: Vec, } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone)] /// The set of literal values a guard can constrain a variable to. /// Used in `ExactGuard` to record variable = value constraints. +/// +/// Guards are only ever produced by `===` / `!==` conditions, so equality on this type is +/// PHP's `===`: floats compare by IEEE value, which makes `0.0` and `-0.0` the same guard +/// (PHP agrees: `0.0 === -0.0` is `true`) and makes NAN equal to nothing, not even itself. +/// A bit-pattern comparison would get both of those backwards and let DCE prune a live branch. pub(super) enum GuardLiteral { Bool(bool), Null, Int(i64), - Float(u64), + Float(f64), String(String), } +impl PartialEq for GuardLiteral { + /// Compares two guard literals with PHP's `===` semantics. + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (GuardLiteral::Bool(left), GuardLiteral::Bool(right)) => left == right, + (GuardLiteral::Null, GuardLiteral::Null) => true, + (GuardLiteral::Int(left), GuardLiteral::Int(right)) => left == right, + (GuardLiteral::Float(left), GuardLiteral::Float(right)) => left == right, + (GuardLiteral::String(left), GuardLiteral::String(right)) => left == right, + _ => false, + } + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] /// Inclusive integer bounds; `None` means ±∞ on that side. pub(super) struct IntInterval { diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index 2fa58c3576..5e43bd6b4e 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -23,6 +23,7 @@ use super::*; pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { let span = stmt.span; let source_mode = stmt.source_mode; + let strict_types = stmt.strict_types; let attributes = stmt.attributes.clone(); let kind = match stmt.kind { StmtKind::Synthetic(stmts) => StmtKind::Synthetic(fold_block(stmts)), @@ -356,6 +357,7 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { kind, span, source_mode, + strict_types, attributes, } } diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index 76eb4f9b3c..a5df433ea0 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -36,19 +36,21 @@ pub(crate) fn prune_block(body: Vec) -> Vec { /// expression statements. Returns a vec to allow statement expansion (e.g., a /// do-while with false condition becoming just its body). pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { - let source_mode = stmt.source_mode; - crate::source::with_parse_mode(source_mode, || prune_stmt_in_source_mode(stmt)) + let profile = stmt.profile(); + crate::source::with_parse_mode(profile, || prune_stmt_in_source_mode(stmt)) } /// Prunes one statement while reconstructed nodes inherit its physical source mode. fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { let span = stmt.span; let source_mode = stmt.source_mode; + let strict_types = stmt.strict_types; match stmt.kind { StmtKind::Echo(expr) => vec![Stmt { kind: StmtKind::Echo(prune_expr(expr)), span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::Assign { name, value } => vec![Stmt { @@ -58,12 +60,14 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::RefAssign { target, source } => vec![Stmt { kind: StmtKind::RefAssign { target, source }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::If { @@ -90,6 +94,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -105,6 +110,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], } @@ -121,6 +127,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], } @@ -145,6 +152,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], } @@ -165,13 +173,14 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::Switch { subject, cases, default, - } => prune_switch_stmt(subject, cases, default, span, source_mode), + } => prune_switch_stmt(subject, cases, default, span, source_mode, strict_types), StmtKind::Try { try_body, catches, @@ -203,6 +212,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }]; } @@ -227,6 +237,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -244,6 +255,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] }; @@ -257,6 +269,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::FunctionDecl { @@ -283,12 +296,14 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::Return(expr) => vec![Stmt { kind: StmtKind::Return(expr.map(prune_expr)), span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::ClassDecl { @@ -323,6 +338,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }] } @@ -333,6 +349,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { kind: StmtKind::ExprStmt(expr), span, source_mode, + strict_types, attributes: Vec::new(), }] } else { @@ -359,12 +376,14 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::PackedClassDecl { name, fields } => vec![Stmt { kind: StmtKind::PackedClassDecl { name, fields }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::InterfaceDecl { @@ -386,6 +405,7 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], StmtKind::TraitDecl { @@ -407,9 +427,10 @@ fn prune_stmt_in_source_mode(stmt: Stmt) -> Vec { }, span, source_mode, + strict_types, attributes: Vec::new(), }], - kind => vec![Stmt { kind, span, source_mode, attributes: Vec::new() }], + kind => vec![Stmt { kind, span, source_mode, strict_types, attributes: Vec::new() }], } } diff --git a/src/optimize/control/switch.rs b/src/optimize/control/switch.rs index 4a83d3dd24..82041ab5f9 100644 --- a/src/optimize/control/switch.rs +++ b/src/optimize/control/switch.rs @@ -23,6 +23,7 @@ pub(crate) fn prune_switch_stmt( default: Option>, span: crate::span::Span, source_mode: crate::source::SourceMode, + strict_types: bool, ) -> Vec { let subject = prune_expr(subject); let cases = normalize_switch_cases(drop_shadowed_switch_patterns(normalize_switch_cases( @@ -48,6 +49,7 @@ pub(crate) fn prune_switch_stmt( }, span, source_mode, + strict_types, attributes: Vec::new(), }]; } @@ -79,6 +81,7 @@ pub(crate) fn prune_switch_stmt( }, span, source_mode, + strict_types, attributes: Vec::new(), }]; }; @@ -97,6 +100,7 @@ pub(crate) fn prune_switch_stmt( }, span, source_mode, + strict_types, attributes: Vec::new(), }]; } @@ -262,28 +266,11 @@ pub(crate) fn compare_scalar_strict(left: &ScalarValue, right: &ScalarValue) -> /// Loose PHP-style switch comparison between two scalar values. /// -/// String compares by value; float compares by numeric value; int is extracted via -/// `scalar_dispatch_int`. Cross-type comparisons between string/float and other types -/// yield `None` (indeterminate). +/// A `switch` case is decided by PHP's `==`, so this is exactly `loose_eq_values`: `case +/// true` matches any truthy subject (`switch (2)` selects it), `case null` matches `0` and +/// `""`, and PHP 8's string/number rules make `case 0` *not* match the subject `"foo"`. +/// Returns `None` only when the pair has no compile-time answer, which keeps the switch on +/// the runtime path. pub(crate) fn compare_scalar_switch(left: &ScalarValue, right: &ScalarValue) -> Option { - match (left, right) { - (ScalarValue::String(left), ScalarValue::String(right)) => Some(left == right), - (ScalarValue::Float(left), ScalarValue::Float(right)) => Some(left == right), - (ScalarValue::String(_), _) | (_, ScalarValue::String(_)) => None, - (ScalarValue::Float(_), _) | (_, ScalarValue::Float(_)) => None, - _ => Some(scalar_dispatch_int(left)? == scalar_dispatch_int(right)?), - } -} - -/// Converts a scalar value to an integer for switch dispatch purposes. -/// -/// Returns `Some(i64)` for Null (as 0), Bool (0/1), and Int values. -/// Returns `None` for Float and String, which cannot be safely coerced in this context. -pub(crate) fn scalar_dispatch_int(value: &ScalarValue) -> Option { - match value { - ScalarValue::Null => Some(0), - ScalarValue::Bool(value) => Some(i64::from(*value)), - ScalarValue::Int(value) => Some(*value), - ScalarValue::Float(_) | ScalarValue::String(_) => None, - } + loose_eq_values(left, right) } diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index d2711082de..d55ada10ea 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -24,6 +24,30 @@ pub(super) use calls::{ static_method_call_effect, }; +/// Returns true when a binary operator can raise a catchable PHP error at runtime. +/// +/// PHP 8 arithmetic is not exception-free: `/` and `%` raise `DivisionByZeroError` for a zero +/// divisor, and `<<` / `>>` raise `ArithmeticError` for a negative shift count. Reporting these +/// as pure would let the try/catch DCE pass (`optimize::control::dce::tries`) drop the very +/// `catch` clause that is supposed to observe them. A literal right operand that is provably +/// safe keeps the expression pure so ordinary arithmetic is not pessimized. +fn binary_op_may_throw(op: &BinOp, right: &Expr) -> bool { + match op { + BinOp::Div | BinOp::Mod => !matches!( + &right.kind, + ExprKind::IntLiteral(value) if *value != 0 + ) && !matches!( + &right.kind, + ExprKind::FloatLiteral(value) if *value != 0.0 + ), + BinOp::ShiftLeft | BinOp::ShiftRight => !matches!( + &right.kind, + ExprKind::IntLiteral(value) if *value >= 0 + ), + _ => false, + } +} + /// Returns true if any statement in `stmts` may throw an exception. /// Shorthand for checking `block_effect(stmts).may_throw`. pub(super) fn block_may_throw(stmts: &[Stmt]) -> bool { @@ -228,7 +252,14 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { ExprKind::Clone(inner) => expr_effect(inner) .with_side_effects() .with_may_throw(), - ExprKind::BinaryOp { left, right, .. } => expr_effect(left).combine(expr_effect(right)), + ExprKind::BinaryOp { left, op, right } => { + let operands = expr_effect(left).combine(expr_effect(right)); + if binary_op_may_throw(op, right) { + operands.with_may_throw() + } else { + operands + } + } ExprKind::InstanceOf { value, target } => { expr_effect(value).combine(instanceof_target_effect(target)) } diff --git a/src/optimize/fold.rs b/src/optimize/fold.rs index c1a9b2ea37..0bd593a916 100644 --- a/src/optimize/fold.rs +++ b/src/optimize/fold.rs @@ -8,13 +8,16 @@ //! Key details: //! - Only fold results that are unambiguous PHP equivalents; division by zero and effectful expressions must remain runtime behavior. +mod array_key; mod casts; +mod compare; mod expr; mod inline_closure; mod ops; mod pipes; mod scalar; +pub(super) use compare::loose_eq_values; pub(super) use expr::{fold_enum_case, fold_expr, fold_method, fold_params, fold_property}; pub(super) use ops::try_fold_array_access; pub(super) use scalar::{assigned_array_fact, assigned_scalar_value, scalar_value, ScalarValue}; diff --git a/src/optimize/fold/array_key.rs b/src/optimize/fold/array_key.rs new file mode 100644 index 0000000000..fabbfea347 --- /dev/null +++ b/src/optimize/fold/array_key.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Normalizes compile-time scalar literals into PHP array keys. +//! Gives the array-literal access fold the same key identity the runtime hash table uses, so +//! `false`/`0`, `"1"`/`1` and `null`/`""` collapse to one slot instead of comparing raw variants. +//! +//! Called from: +//! - `crate::optimize::fold::ops` +//! +//! Key details: +//! - The integer-string rule is shared with the type checker through +//! `crate::types::is_php_integer_array_key`; there is exactly one definition of +//! "this string is really an int key" in the compiler. +//! - Float keys truncate toward zero, but PHP 8.1+ *deprecates* the lossy ones. Folding one +//! would swallow the diagnostic, so only exactly-representable floats normalize. + +use crate::types::is_php_integer_array_key; + +use super::scalar::ScalarValue; + +/// A normalized PHP array key: the hash table only ever stores integers and strings. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum PhpArrayKey { + Int(i64), + Str(String), +} + +/// Normalizes a scalar literal into the array key PHP would actually store. +/// +/// `null` becomes `""`, booleans become `0`/`1`, integer-valued strings become integers, and +/// integral in-range floats truncate. Returns `None` for a float that PHP would report as a +/// lossy implicit conversion (fractional or out of `i64` range) so the fold declines instead +/// of swallowing the deprecation notice. +pub(super) fn php_array_key(value: &ScalarValue) -> Option { + match value { + ScalarValue::Null => Some(PhpArrayKey::Str(String::new())), + ScalarValue::Bool(value) => Some(PhpArrayKey::Int(i64::from(*value))), + ScalarValue::Int(value) => Some(PhpArrayKey::Int(*value)), + ScalarValue::Float(value) => float_array_key(*value), + ScalarValue::String(value) => Some(if is_php_integer_array_key(value) { + PhpArrayKey::Int(value.parse::().ok()?) + } else { + PhpArrayKey::Str(value.clone()) + }), + } +} + +/// Normalizes a float array key, declining every value PHP 8.1+ deprecates. +/// +/// Only a finite float that is already integral and inside the `i64` range converts silently; +/// `1.7` and `1e20` both emit "Implicit conversion from float ... loses precision", which a +/// folded access would hide. +fn float_array_key(value: f64) -> Option { + if !value.is_finite() || value.trunc() != value { + return None; + } + // `i64::MAX as f64` rounds up, so the upper bound is exclusive. + if value >= -(i64::MIN as f64) || value < i64::MIN as f64 { + return None; + } + Some(PhpArrayKey::Int(value as i64)) +} diff --git a/src/optimize/fold/casts.rs b/src/optimize/fold/casts.rs index 8eba83768a..6566af4a0d 100644 --- a/src/optimize/fold/casts.rs +++ b/src/optimize/fold/casts.rs @@ -9,6 +9,7 @@ //! - Folding must respect PHP coercions, truthiness, numeric edge cases, and runtime error boundaries. use super::super::*; +use super::compare::{php_numeric_prefix, PhpNumeric}; use super::scalar::{scalar_value, ScalarValue}; /// Attempts to constant-fold a cast expression. @@ -95,35 +96,48 @@ fn truncate_float_to_i64(value: f64) -> Option { Some(truncated as i64) } -/// Parses a string value for `(int)` cast folding. +/// Parses a string value for `(int)` cast folding, reproducing PHP's `zval_get_long()`. /// - /// Tries i64 parse first, then f64 parse with truncation, then falls back to - /// all-alphabetic strings (which PHP treats as `0`). Returns `None` for strings - /// that contain digits or mixed digit/alpha content that fail numeric parsing. + /// The string's leading numeric run decides the result: an integer run that fits `i64` is + /// used directly, anything else goes through `zend_dval_to_lval_cap` (NAN/INF become `0`, + /// out-of-range floats saturate). A string with no numeric prefix at all is `0`, so + /// `"abc"`, `"0x1A"` and `"INF"` all fold to `0` rather than being parsed as Rust numbers. fn parse_string_cast_int(value: &str) -> Option { - if let Ok(parsed) = value.parse::() { - return Some(parsed); - } - if let Ok(parsed) = value.parse::() { - return truncate_float_to_i64(parsed); - } - if value.chars().all(|ch| ch.is_ascii_alphabetic()) { - return Some(0); - } - None + Some(match php_numeric_prefix(value) { + Some(PhpNumeric::Int(parsed)) => parsed, + Some(PhpNumeric::Float { value, .. }) => cap_float_to_i64(value), + None => 0, + }) } -/// Parses a string value for `(float)` cast folding. +/// Parses a string value for `(float)` cast folding, reproducing PHP's `zend_strtod()`. /// - /// Tries f64 parse first; if that fails and all characters are alphabetic, returns `0.0`. - /// Any other pattern (mixed digits/alpha, punctuation, etc.) returns `None` so the - /// cast is evaluated at runtime. + /// Only the leading numeric run counts, and PHP's grammar has no `INF`, `NAN`, hexadecimal + /// or underscore forms — unlike Rust's `str::parse::()`, which accepts `"inf"` and + /// `"nan"`. A string with no numeric prefix is `0.0`. fn parse_string_cast_float(value: &str) -> Option { - if let Ok(parsed) = value.parse::() { - return Some(parsed); + Some(match php_numeric_prefix(value) { + Some(PhpNumeric::Int(parsed)) => parsed as f64, + Some(PhpNumeric::Float { value, .. }) => value, + None => 0.0, + }) +} + +/// Saturating float-to-int conversion, PHP's `zend_dval_to_lval_cap()`. + /// + /// Non-finite values become `0`; values outside the `i64` range clamp to `PHP_INT_MAX` / + /// `PHP_INT_MIN`; everything else truncates toward zero. Used only by the string `(int)` + /// cast, which is the one PHP path that saturates instead of wrapping. +fn cap_float_to_i64(value: f64) -> i64 { + if !value.is_finite() { + return 0; + } + // `i64::MAX as f64` rounds up to 2^63, so the upper bound is exclusive. + if value >= -(i64::MIN as f64) { + return i64::MAX; } - if value.chars().all(|ch| ch.is_ascii_alphabetic()) { - return Some(0.0); + if value < i64::MIN as f64 { + return i64::MIN; } - None + value as i64 } diff --git a/src/optimize/fold/compare.rs b/src/optimize/fold/compare.rs new file mode 100644 index 0000000000..c501e14bd5 --- /dev/null +++ b/src/optimize/fold/compare.rs @@ -0,0 +1,342 @@ +//! Purpose: +//! Reimplements PHP 8's `zend_compare()` over compile-time scalar literals. +//! Owns numeric-string recognition, integer-exact ordering, and the loose (`==`) result +//! used by binary-operator folding, `switch` case selection, and array-key coercion. +//! +//! Called from: +//! - `crate::optimize::fold::scalar` +//! - `crate::optimize::fold::ops` +//! - `crate::optimize::fold::casts` +//! - `crate::optimize::control::switch` +//! +//! Key details: +//! - Integers are compared as `i64`, never through `f64`: `PHP_INT_MAX - 1 < PHP_INT_MAX` +//! is observable and a `f64` round-trip loses it. +//! - PHP 8 compares an int/float against a *non-numeric* string by stringifying the number, +//! so `0 == "abc"` is `false`. Float stringification is precision-dependent, so that one +//! pair declines the fold instead of guessing. +//! - `ZEND_THREEWAY_COMPARE` answers `1` for any NAN pair; `<`/`<=`/`>`/`>=` are therefore +//! spelled through `zend_compare` argument order (PHP implements `a > b` as `b < a`). + +use std::cmp::Ordering; + +use super::scalar::ScalarValue; + +/// PHP's `is_numeric_string()` classification of a numeric string. +/// +/// `Int` mirrors `IS_LONG`; `Float` mirrors `IS_DOUBLE`, carrying the `oflow` flag PHP +/// sets when the text was a plain integer too large (`1`) or too small (`-1`) for `i64`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(in crate::optimize) enum PhpNumeric { + Int(i64), + Float { value: f64, integer_overflow: i8 }, +} + +impl PhpNumeric { + /// Returns the value as an `f64`, matching PHP's `dval` slot for either classification. + fn as_f64(self) -> f64 { + match self { + PhpNumeric::Int(value) => value as f64, + PhpNumeric::Float { value, .. } => value, + } + } + + /// Returns PHP's `oflow` marker: non-zero only for integer text that exceeded `i64`. + fn integer_overflow(self) -> i8 { + match self { + PhpNumeric::Int(_) => 0, + PhpNumeric::Float { + integer_overflow, .. + } => integer_overflow, + } + } +} + +/// Returns whether a byte is one of the six characters PHP's `ZEND_IS_WHITESPACE` accepts. +fn is_php_whitespace(byte: u8) -> bool { + matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c) +} + +/// The longest leading numeric run of a string, as scanned by PHP's `_is_numeric_string_ex`. +struct NumericScan<'a> { + /// The matched numeric text with leading whitespace and trailing garbage removed. + text: &'a str, + /// Whether the run contained a decimal point or a consumed exponent (`IS_DOUBLE` syntax). + is_float: bool, + /// Everything after the matched run, still un-trimmed. + trailing: &'a str, +} + +/// Scans the longest leading numeric run of `value` using PHP's numeric-string grammar. +/// +/// Accepts optional leading whitespace, an optional sign, a mantissa with at least one +/// digit (`12`, `.5`, `5.`), and an exponent only when at least one digit follows it — +/// `"1e"` therefore scans as `1` with `"e"` left over, exactly like PHP. Hex, underscore +/// separators, `INF` and `NAN` are not part of the grammar. Returns `None` when no digit +/// is present at all. +fn scan_numeric_prefix(value: &str) -> Option> { + let bytes = value.as_bytes(); + let mut idx = 0; + while idx < bytes.len() && is_php_whitespace(bytes[idx]) { + idx += 1; + } + + let start = idx; + if idx < bytes.len() && matches!(bytes[idx], b'+' | b'-') { + idx += 1; + } + + let mut digits = 0; + while idx < bytes.len() && bytes[idx].is_ascii_digit() { + idx += 1; + digits += 1; + } + + let mut is_float = false; + if idx < bytes.len() && bytes[idx] == b'.' { + let mut probe = idx + 1; + while probe < bytes.len() && bytes[probe].is_ascii_digit() { + probe += 1; + digits += 1; + } + if digits > 0 { + idx = probe; + is_float = true; + } + } + if digits == 0 { + return None; + } + + if idx < bytes.len() && matches!(bytes[idx], b'e' | b'E') { + let mut probe = idx + 1; + if probe < bytes.len() && matches!(bytes[probe], b'+' | b'-') { + probe += 1; + } + let exponent_start = probe; + while probe < bytes.len() && bytes[probe].is_ascii_digit() { + probe += 1; + } + if probe > exponent_start { + idx = probe; + is_float = true; + } + } + + Some(NumericScan { + text: &value[start..idx], + is_float, + trailing: &value[idx..], + }) +} + +/// Classifies a scanned numeric run into PHP's `IS_LONG` / `IS_DOUBLE` result. +/// +/// Integer text that does not fit `i64` becomes `Float` with the `oflow` sign PHP records, +/// which `zendi_smart_strcmp` needs to fall back to a byte comparison. +fn classify_numeric_scan(scan: &NumericScan<'_>) -> Option { + if scan.is_float { + // Rust's `f64` parser accepts the exact grammar scanned above and, like + // `zend_strtod`, saturates to infinity on exponent overflow. + return scan.text.parse::().ok().map(|value| PhpNumeric::Float { + value, + integer_overflow: 0, + }); + } + match scan.text.parse::() { + Ok(value) => Some(PhpNumeric::Int(value)), + Err(_) => scan.text.parse::().ok().map(|value| PhpNumeric::Float { + value, + integer_overflow: if scan.text.starts_with('-') { -1 } else { 1 }, + }), + } +} + +/// Recognizes a *fully* numeric string, PHP's `is_numeric_string(..., allow_errors = 0)`. +/// +/// Leading and trailing whitespace are permitted (PHP 8 accepts `"1 "`), any other trailing +/// byte makes the string non-numeric. Returns `None` for `""`, `"abc"`, `"1abc"`, `"0x1A"`, +/// `"1_000"`, `"INF"`, and `"NAN"`. +pub(in crate::optimize) fn php_numeric_string(value: &str) -> Option { + let scan = scan_numeric_prefix(value)?; + if !scan.trailing.bytes().all(is_php_whitespace) { + return None; + } + classify_numeric_scan(&scan) +} + +/// Recognizes a *leading* numeric run, PHP's `is_numeric_string(..., allow_errors = 1)`. +/// +/// This is the form the `(int)` / `(float)` string casts use, so `"12abc"` yields `12` and +/// `"1.2.3"` yields `1.2`. Returns `None` when the string has no numeric prefix, which the +/// casts turn into `0` / `0.0`. +pub(in crate::optimize) fn php_numeric_prefix(value: &str) -> Option { + let scan = scan_numeric_prefix(value)?; + classify_numeric_scan(&scan) +} + +/// PHP's `ZEND_THREEWAY_COMPARE`: equal, less, otherwise greater. +/// +/// Any NAN operand falls into the `Greater` arm, which is what makes `NAN < 1`, `NAN > 1`, +/// `NAN <= 1` and `NAN >= 1` all evaluate to `false` once the caller spells the operator +/// through `zend_compare` argument order. +fn three_way(left: f64, right: f64) -> Ordering { + if left == right { + Ordering::Equal + } else if left < right { + Ordering::Less + } else { + Ordering::Greater + } +} + +/// PHP's `zend_binary_strcmp`: byte-wise `memcmp` with the shorter string ordered first on a +/// common prefix. +fn compare_bytes(left: &str, right: &str) -> Ordering { + left.as_bytes().cmp(right.as_bytes()) +} + +/// PHP's `zendi_smart_strcmp`: two numeric strings compare numerically, anything else +/// compares byte-wise. +/// +/// Two integer strings compare exactly as `i64`, so `"9223372036854775806"` is genuinely +/// smaller than `"9223372036854775807"`. Values that both overflowed `i64` in the same +/// direction, or that are both infinite, fall back to the byte comparison exactly as PHP +/// does to avoid the accuracy loss of the `f64` round-trip. +fn compare_strings(left: &str, right: &str) -> Ordering { + let (Some(left_num), Some(right_num)) = (php_numeric_string(left), php_numeric_string(right)) + else { + return compare_bytes(left, right); + }; + + let (left_overflow, right_overflow) = (left_num.integer_overflow(), right_num.integer_overflow()); + if left_overflow != 0 + && left_overflow == right_overflow + && left_num.as_f64() - right_num.as_f64() == 0.0 + { + return compare_bytes(left, right); + } + + match (left_num, right_num) { + (PhpNumeric::Int(left_value), PhpNumeric::Int(right_value)) => left_value.cmp(&right_value), + (PhpNumeric::Int(_), PhpNumeric::Float { .. }) if right_overflow != 0 => { + // The right operand is an integer beyond `i64`; its sign decides the order. + if right_overflow > 0 { + Ordering::Less + } else { + Ordering::Greater + } + } + (PhpNumeric::Float { .. }, PhpNumeric::Int(_)) if left_overflow != 0 => { + if left_overflow > 0 { + Ordering::Greater + } else { + Ordering::Less + } + } + _ => { + let (left_value, right_value) = (left_num.as_f64(), right_num.as_f64()); + if left_value == right_value && !left_value.is_finite() { + return compare_bytes(left, right); + } + three_way(left_value - right_value, 0.0) + } + } +} + +/// PHP's `compare_long_to_string`. +/// +/// A numeric string compares against the integer as a number (exactly when the string is +/// itself an integer); a non-numeric string makes PHP 8 stringify the integer and compare +/// bytes, which is why `0 == "abc"` is `false`. +fn compare_int_to_string(left: i64, right: &str) -> Ordering { + match php_numeric_string(right) { + Some(PhpNumeric::Int(right)) => left.cmp(&right), + Some(PhpNumeric::Float { value, .. }) => three_way(left as f64, value), + None => compare_bytes(&left.to_string(), right), + } +} + +/// PHP's `compare_double_to_string`. +/// +/// Returns `None` for a non-numeric string: PHP stringifies the float through +/// `zend_double_to_str`, whose output (`"INF"`, `"1.0E+25"`, …) depends on formatting rules +/// this fold deliberately does not reimplement, so the comparison stays on the runtime path. +fn compare_float_to_string(left: f64, right: &str) -> Option { + match php_numeric_string(right) { + Some(PhpNumeric::Int(right)) => Some(three_way(left - right as f64, 0.0)), + Some(PhpNumeric::Float { value, .. }) => Some(three_way(left, value)), + None => None, + } +} + +/// PHP 8's `zend_compare()` over two scalar literals. +/// +/// Returns `None` only when the result cannot be established at compile time (a float +/// against a non-numeric string). Callers must spell the relational operators the way the +/// engine does — `a > b` is `compare(b, a) == Less` — so NAN keeps PHP's behavior. +pub(in crate::optimize) fn compare_scalars( + left: &ScalarValue, + right: &ScalarValue, +) -> Option { + match (left, right) { + (ScalarValue::Int(left), ScalarValue::Int(right)) => Some(left.cmp(right)), + (ScalarValue::Int(left), ScalarValue::Float(right)) => Some(three_way(*left as f64, *right)), + (ScalarValue::Float(left), ScalarValue::Int(right)) => Some(three_way(*left, *right as f64)), + (ScalarValue::Float(left), ScalarValue::Float(right)) => Some(three_way(*left, *right)), + (ScalarValue::String(left), ScalarValue::String(right)) => Some(compare_strings(left, right)), + // PHP special-cases null against a string: only the empty string compares equal, so + // `null == "0"` is false even though `"0"` is falsy. + (ScalarValue::Null, ScalarValue::String(right)) => Some(if right.is_empty() { + Ordering::Equal + } else { + Ordering::Less + }), + (ScalarValue::String(left), ScalarValue::Null) => Some(if left.is_empty() { + Ordering::Equal + } else { + Ordering::Greater + }), + (ScalarValue::Int(left), ScalarValue::String(right)) => { + Some(compare_int_to_string(*left, right)) + } + (ScalarValue::String(left), ScalarValue::Int(right)) => { + Some(compare_int_to_string(*right, left).reverse()) + } + (ScalarValue::Float(left), ScalarValue::String(right)) => { + compare_float_to_string(*left, right) + } + (ScalarValue::String(left), ScalarValue::Float(right)) => { + compare_float_to_string(*right, left).map(Ordering::reverse) + } + // Everything else goes through PHP's boolean fallback in `zend_compare`'s default arm. + (ScalarValue::Null | ScalarValue::Bool(false), right) => Some(if right.truthy() { + Ordering::Less + } else { + Ordering::Equal + }), + (ScalarValue::Bool(true), right) => Some(if right.truthy() { + Ordering::Equal + } else { + Ordering::Greater + }), + (left, ScalarValue::Null | ScalarValue::Bool(false)) => Some(if left.truthy() { + Ordering::Greater + } else { + Ordering::Equal + }), + (left, ScalarValue::Bool(true)) => Some(if left.truthy() { + Ordering::Equal + } else { + Ordering::Less + }), + } +} + +/// PHP's `==` over two scalar literals, i.e. `zend_compare(...) == 0`. +pub(in crate::optimize) fn loose_eq_values( + left: &ScalarValue, + right: &ScalarValue, +) -> Option { + compare_scalars(left, right).map(|ordering| ordering == Ordering::Equal) +} diff --git a/src/optimize/fold/ops.rs b/src/optimize/fold/ops.rs index 7e69b58ff2..cc1a08b1ea 100644 --- a/src/optimize/fold/ops.rs +++ b/src/optimize/fold/ops.rs @@ -8,16 +8,25 @@ //! Key details: //! - Folding must respect PHP coercions, truthiness, numeric edge cases, and runtime error boundaries. +use std::cmp::Ordering; + use super::super::*; +use super::array_key::{php_array_key, PhpArrayKey}; use super::scalar::{ - compare_numeric, int_literal, loose_eq, numeric_literal, scalar_value, spaceship_numeric, + compare_scalar_exprs, int_literal, loose_eq, numeric_literal, scalar_value, spaceship_scalar, strict_eq, ScalarValue, }; -/// Returns the negated literal if the expression is an int or float literal that can be negated without overflow. +/// Returns the negated literal if the expression is an int or float literal. +/// +/// `-PHP_INT_MIN` is not representable as an `i64`; PHP promotes it to the float +/// `9223372036854775808.0` rather than wrapping, so the fold does the same. pub(super) fn try_fold_negate(expr: &Expr) -> Option { match &expr.kind { - ExprKind::IntLiteral(value) => value.checked_neg().map(ExprKind::IntLiteral), + ExprKind::IntLiteral(value) => Some(match value.checked_neg() { + Some(negated) => ExprKind::IntLiteral(negated), + None => ExprKind::FloatLiteral(-(*value as f64)), + }), ExprKind::FloatLiteral(value) => Some(ExprKind::FloatLiteral(-value)), _ => None, } @@ -105,7 +114,7 @@ fn try_fold_numeric_binop(op: &BinOp, left: &Expr, right: &Expr) -> Option Option { match op { BinOp::Add => left @@ -120,23 +129,80 @@ fn try_fold_int_numeric_binop(op: &BinOp, left: i64, right: i64) -> Option { - if right == 0 { - None - } else { - Some(ExprKind::FloatLiteral(left as f64 / right as f64)) + BinOp::Div => try_fold_int_div(left, right), + BinOp::Pow => try_fold_int_pow(left, right), + _ => None, + } +} + +/// Folds integer `/` the way PHP's `div_function` does. +/// +/// An exact division stays an integer (`6 / 3` is `int(2)`, not `float(2)`); anything else +/// becomes a float. `PHP_INT_MIN / -1` has no `i64` result, so it also becomes a float +/// instead of overflowing. Division by zero declines so the runtime raises +/// `DivisionByZeroError`. +fn try_fold_int_div(left: i64, right: i64) -> Option { + if right == 0 { + return None; + } + match (left.checked_rem(right), left.checked_div(right)) { + (Some(0), Some(quotient)) => Some(ExprKind::IntLiteral(quotient)), + _ => Some(ExprKind::FloatLiteral(left as f64 / right as f64)), + } +} + +/// Folds integer `**` the way PHP's `pow_function_base` does. +/// +/// A non-negative exponent keeps an integer result when it fits (`2 ** 3` is `int(8)`, not +/// `float(8)`). On overflow PHP does *not* fall back to a single `pow()` call — it multiplies +/// the exact integer accumulated so far by `pow()` of the remaining factor, and the two differ +/// in the last ULP for most inputs, so the square-and-multiply loop is reproduced verbatim. +/// A negative exponent is plain `pow((double) base, (double) exp)` in PHP. A non-finite result +/// declines, leaving the operation on the runtime path. +fn try_fold_int_pow(left: i64, right: i64) -> Option { + if right < 0 { + let result = (left as f64).powf(right as f64); + return result.is_finite().then_some(ExprKind::FloatLiteral(result)); + } + if right == 0 { + return Some(ExprKind::IntLiteral(1)); + } + if left == 0 { + return Some(ExprKind::IntLiteral(0)); + } + + let (mut accumulated, mut factor, mut exponent) = (1i64, left, right); + while exponent >= 1 { + if exponent % 2 == 1 { + exponent -= 1; + match accumulated.checked_mul(factor) { + Some(product) => accumulated = product, + None => { + let overflowed = accumulated as f64 * factor as f64; + return finite_float_literal(overflowed * (factor as f64).powf(exponent as f64)); + } } - } - BinOp::Pow => { - let result = (left as f64).powf(right as f64); - if result.is_finite() { - Some(ExprKind::FloatLiteral(result)) - } else { - None + } else { + exponent /= 2; + match factor.checked_mul(factor) { + Some(product) => factor = product, + None => { + let overflowed = factor as f64 * factor as f64; + return finite_float_literal(accumulated as f64 * overflowed.powf(exponent as f64)); + } } } - _ => None, + if exponent == 0 { + return Some(ExprKind::IntLiteral(accumulated)); + } } + // The loop always returns through the `exponent == 0` check above. + None +} + +/// Wraps a computed float in a literal, declining when it is not finite. +fn finite_float_literal(value: f64) -> Option { + value.is_finite().then_some(ExprKind::FloatLiteral(value)) } /// Converts overflowed integer add/sub/mul operations to float to match PHP's numeric coercion. @@ -151,32 +217,42 @@ fn fold_int_overflow_to_float(op: &BinOp, left: i64, right: i64) -> Option Option { let (left, right) = (int_literal(left)?, int_literal(right)?); - if right == 0 { - None - } else { - Some(ExprKind::IntLiteral(left % right)) + match right { + 0 => None, + -1 => Some(ExprKind::IntLiteral(0)), + _ => Some(ExprKind::IntLiteral(left % right)), } } /// Evaluates bitwise AND, OR, XOR, and shift operations on two integer literals. -/// Shift amounts must fit in a `u32`; returns `None` for invalid shift amounts. +/// +/// Shifts follow PHP's `shift_left_function` / `shift_right_function`: a negative shift +/// declines so the runtime raises `ArithmeticError`, a shift of 64 or more yields `0` +/// (`-1` for a right shift of a negative value), and in-range shifts wrap like the engine's +/// native `<<` / `>>`. fn try_fold_bitwise_binop(op: &BinOp, left: &Expr, right: &Expr) -> Option { let (left, right) = (int_literal(left)?, int_literal(right)?); match op { BinOp::BitAnd => Some(ExprKind::IntLiteral(left & right)), BinOp::BitOr => Some(ExprKind::IntLiteral(left | right)), BinOp::BitXor => Some(ExprKind::IntLiteral(left ^ right)), - BinOp::ShiftLeft => { - let shift = u32::try_from(right).ok()?; - left.checked_shl(shift).map(ExprKind::IntLiteral) - } - BinOp::ShiftRight => { - let shift = u32::try_from(right).ok()?; - left.checked_shr(shift).map(ExprKind::IntLiteral) - } + BinOp::ShiftLeft => match u32::try_from(right).ok()? { + shift if shift >= i64::BITS => Some(ExprKind::IntLiteral(0)), + shift => Some(ExprKind::IntLiteral(left.wrapping_shl(shift))), + }, + BinOp::ShiftRight => match u32::try_from(right).ok()? { + shift if shift >= i64::BITS => { + Some(ExprKind::IntLiteral(if left < 0 { -1 } else { 0 })) + } + shift => Some(ExprKind::IntLiteral(left >> shift)), + }, _ => None, } } @@ -197,17 +273,29 @@ fn try_fold_logical_binop(op: &BinOp, left: &Expr, right: &Expr) -> Option b` as `b < a` and `a >= b` as `b <= a`, so the relational arms swap +/// the operands rather than inverting the ordering. That is what keeps every NAN comparison +/// false: `zend_compare` answers `1` for any NAN pair in either direction. fn try_fold_compare_binop(op: &BinOp, left: &Expr, right: &Expr) -> Option { match op { BinOp::Eq => Some(ExprKind::BoolLiteral(loose_eq(left, right)?)), BinOp::NotEq => Some(ExprKind::BoolLiteral(!loose_eq(left, right)?)), BinOp::StrictEq => Some(ExprKind::BoolLiteral(strict_eq(left, right)?)), BinOp::StrictNotEq => Some(ExprKind::BoolLiteral(!strict_eq(left, right)?)), - BinOp::Lt => Some(ExprKind::BoolLiteral(compare_numeric(left, right, |l, r| l < r)?)), - BinOp::Gt => Some(ExprKind::BoolLiteral(compare_numeric(left, right, |l, r| l > r)?)), - BinOp::LtEq => Some(ExprKind::BoolLiteral(compare_numeric(left, right, |l, r| l <= r)?)), - BinOp::GtEq => Some(ExprKind::BoolLiteral(compare_numeric(left, right, |l, r| l >= r)?)), - BinOp::Spaceship => Some(ExprKind::IntLiteral(spaceship_numeric(left, right)?)), + BinOp::Lt => Some(ExprKind::BoolLiteral( + compare_scalar_exprs(left, right)? == Ordering::Less, + )), + BinOp::Gt => Some(ExprKind::BoolLiteral( + compare_scalar_exprs(right, left)? == Ordering::Less, + )), + BinOp::LtEq => Some(ExprKind::BoolLiteral( + compare_scalar_exprs(left, right)? != Ordering::Greater, + )), + BinOp::GtEq => Some(ExprKind::BoolLiteral( + compare_scalar_exprs(right, left)? != Ordering::Greater, + )), + BinOp::Spaceship => Some(ExprKind::IntLiteral(spaceship_scalar(left, right)?)), _ => None, } } @@ -258,10 +346,15 @@ pub(in crate::optimize) fn try_fold_array_access(array: &Expr, index: &Expr) -> } } -/// Returns the array element at a given numeric index when all array elements and the index are scalar literals. -/// Only succeeds if every element in the array is a scalar literal (required to guarantee the result is foldable). +/// Returns the array element at a given index when all array elements and the index are scalar +/// literals. +/// +/// The index is normalized through PHP's array-key rules first, so `["a", "b"][true]` and +/// `["a", "b"]["1"]` both select `"b"`. Only succeeds if every element in the array is a +/// scalar literal (required to guarantee the result is foldable) and the index is in range; +/// an out-of-range index declines so the runtime still emits "Undefined array key". fn try_fold_indexed_array_access(items: &[Expr], index: &Expr) -> Option { - let ScalarValue::Int(index) = scalar_value(index)? else { + let PhpArrayKey::Int(index) = php_array_key(&scalar_value(index)?)? else { return None; }; let index = usize::try_from(index).ok()?; @@ -274,14 +367,19 @@ fn try_fold_indexed_array_access(items: &[Expr], index: &Expr) -> Option "a", false => +/// "b"]` really is a one-entry array and `[0]` selects `"b"`. Duplicate normalized keys are +/// last-wins, matching the order the literal is built in. Declines whenever a key cannot be +/// normalized (a lossy float key) or nothing matches (the runtime owns the warning). fn try_fold_assoc_array_access(items: &[(Expr, Expr)], index: &Expr) -> Option { - let index = scalar_value(index)?; + let index = php_array_key(&scalar_value(index)?)?; let mut selected = None; for (key, value) in items { - let key = scalar_value(key)?; + let key = php_array_key(&scalar_value(key)?)?; let value = scalar_value(value)?; if key == index { selected = Some(value); diff --git a/src/optimize/fold/scalar.rs b/src/optimize/fold/scalar.rs index bd0a7b80ac..17344f9cd3 100644 --- a/src/optimize/fold/scalar.rs +++ b/src/optimize/fold/scalar.rs @@ -8,7 +8,10 @@ //! Key details: //! - Folding must respect PHP coercions, truthiness, numeric edge cases, and runtime error boundaries. +use std::cmp::Ordering; + use super::super::*; +use super::compare::{compare_scalars, loose_eq_values}; /// Extracts an i64 from an integer literal expression. pub(in crate::optimize) fn int_literal(expr: &Expr) -> Option { @@ -53,7 +56,7 @@ pub(in crate::optimize) fn assigned_scalar_value(expr: &Expr) -> Option { let then_value = assigned_scalar_value(then_expr)?; let else_value = assigned_scalar_value(else_expr)?; - (then_value == else_value).then_some(then_value) + then_value.same_constant(&else_value).then_some(then_value) } ExprKind::ShortTernary { value, default } => { let value = assigned_scalar_value(value)?; @@ -67,7 +70,10 @@ pub(in crate::optimize) fn assigned_scalar_value(expr: &Expr) -> Option None, @@ -112,127 +118,32 @@ pub(in crate::optimize) fn strict_eq(left: &Expr, right: &Expr) -> Option } /// Returns `Some(true)` if two scalar expressions are loosely equal (==) per PHP coercion rules, -/// `Some(false)` if not, or `None` if either operand is not a scalar literal. +/// `Some(false)` if not, or `None` if either operand is not a scalar literal or the pair has no +/// compile-time answer (a float against a non-numeric string). pub(in crate::optimize) fn loose_eq(left: &Expr, right: &Expr) -> Option { let left = scalar_value(left)?; let right = scalar_value(right)?; - match (&left, &right) { - (ScalarValue::Bool(left), right) => Some(*left == right.truthy()), - (left, ScalarValue::Bool(right)) => Some(left.truthy() == *right), - (ScalarValue::Null, ScalarValue::Null) => Some(true), - (ScalarValue::Null, ScalarValue::String(right)) => Some(right.is_empty()), - (ScalarValue::String(left), ScalarValue::Null) => Some(left.is_empty()), - (ScalarValue::Null, ScalarValue::Int(right)) => Some(*right == 0), - (ScalarValue::Int(left), ScalarValue::Null) => Some(*left == 0), - (ScalarValue::Null, ScalarValue::Float(right)) => Some(*right == 0.0), - (ScalarValue::Float(left), ScalarValue::Null) => Some(*left == 0.0), - (ScalarValue::String(left), ScalarValue::String(right)) => { - match (php_numeric_string(left), php_numeric_string(right)) { - (Some(left), Some(right)) => Some(left == right), - _ => Some(left == right), - } - } - (ScalarValue::Int(left), ScalarValue::Int(right)) => Some(left == right), - (ScalarValue::Float(left), ScalarValue::Float(right)) => Some(left == right), - (ScalarValue::Int(left), ScalarValue::Float(right)) => Some(*left as f64 == *right), - (ScalarValue::Float(left), ScalarValue::Int(right)) => Some(*left == *right as f64), - (ScalarValue::Int(left), ScalarValue::String(right)) => { - php_numeric_string(right).map(|right| *left as f64 == right).or(Some(false)) - } - (ScalarValue::String(left), ScalarValue::Int(right)) => { - php_numeric_string(left).map(|left| left == *right as f64).or(Some(false)) - } - (ScalarValue::Float(left), ScalarValue::String(right)) => { - php_numeric_string(right).map(|right| *left == right).or(Some(false)) - } - (ScalarValue::String(left), ScalarValue::Float(right)) => { - php_numeric_string(left).map(|left| left == *right).or(Some(false)) - } - } + loose_eq_values(&left, &right) } -/// Parses a numeric string (int or float) from a trimmed string, returning the f64 value or None if the string is not purely numeric. +/// Returns PHP's `zend_compare()` ordering for two scalar literal expressions. /// -/// Handles leading sign, integer part, optional fractional part, and optional exponent. -/// Returns `None` for non-numeric strings, empty strings, or non-finite results. -fn php_numeric_string(value: &str) -> Option { - let trimmed = value.trim_matches(|c: char| c.is_ascii_whitespace()); - if trimmed.is_empty() { - return None; - } - - let bytes = trimmed.as_bytes(); - let mut idx = 0; - if matches!(bytes[idx], b'+' | b'-') { - idx += 1; - if idx == bytes.len() { - return None; - } - } - - let mut digits = 0; - while idx < bytes.len() && bytes[idx].is_ascii_digit() { - idx += 1; - digits += 1; - } - - if idx < bytes.len() && bytes[idx] == b'.' { - idx += 1; - while idx < bytes.len() && bytes[idx].is_ascii_digit() { - idx += 1; - digits += 1; - } - } - if digits == 0 { - return None; - } - - if idx < bytes.len() && matches!(bytes[idx], b'e' | b'E') { - idx += 1; - if idx < bytes.len() && matches!(bytes[idx], b'+' | b'-') { - idx += 1; - } - let exp_start = idx; - while idx < bytes.len() && bytes[idx].is_ascii_digit() { - idx += 1; - } - if idx == exp_start { - return None; - } - } - - if idx != bytes.len() { - return None; - } - trimmed.parse::().ok().filter(|value| value.is_finite()) -} - -/// Returns `Some(true)` if two numeric expressions satisfy the given comparison function, -/// or `None` if either operand is not a numeric literal. -pub(in crate::optimize) fn compare_numeric( - left: &Expr, - right: &Expr, - cmp: impl FnOnce(f64, f64) -> bool, -) -> Option { - let left = numeric_literal(left)?; - let right = numeric_literal(right)?; - Some(cmp(left, right)) +/// Returns `None` when either operand is not a scalar literal, or when the pair has no +/// compile-time answer. Relational folding must pass the operands in the order the engine +/// uses (`a > b` is `compare(b, a) == Less`) so NAN keeps PHP's behavior. +pub(in crate::optimize) fn compare_scalar_exprs(left: &Expr, right: &Expr) -> Option { + let left = scalar_value(left)?; + let right = scalar_value(right)?; + compare_scalars(&left, &right) } -/// Returns the result of the spaceship operator (<=>) on two numeric literals as an i64 (-1, 0, or 1), -/// or `None` if either operand is not a numeric literal. -pub(in crate::optimize) fn spaceship_numeric(left: &Expr, right: &Expr) -> Option { - let left = numeric_literal(left)?; - let right = numeric_literal(right)?; - Some(if left.is_nan() || right.is_nan() { - // NAN is uncomparable: PHP's `<=>` yields 1 whenever either operand is NAN. - 1 - } else if left < right { - -1 - } else if left > right { - 1 - } else { - 0 +/// Returns the result of the spaceship operator (`<=>`) on two scalar literals as -1, 0, or 1, +/// or `None` when the comparison has no compile-time answer. +pub(in crate::optimize) fn spaceship_scalar(left: &Expr, right: &Expr) -> Option { + Some(match compare_scalar_exprs(left, right)? { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, }) } @@ -259,6 +170,22 @@ impl ScalarValue { } } + /// Returns whether two scalar values denote the *same constant*, i.e. whether one can be + /// substituted for the other without changing any observable byte of the program. + /// + /// This is deliberately stricter than `PartialEq`: floats are compared by bit pattern, so + /// `0.0` and `-0.0` stay distinct (`echo -0.0` prints `-0`) and two NANs with the same + /// payload merge. Use it for constant identity — merging ternary/match arms into one + /// propagated fact — never for PHP's `==` or `===`, which are value comparisons. + pub(in crate::optimize) fn same_constant(&self, other: &Self) -> bool { + match (self, other) { + (ScalarValue::Float(left), ScalarValue::Float(right)) => { + left.to_bits() == right.to_bits() + } + (left, right) => left == right, + } + } + /// Returns whether this scalar value is a floating-point NAN. /// /// Folding a NAN to bool is value-correct (`truthy()` already answers `true`, since diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index 0314904085..09b07a8f19 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -524,6 +524,7 @@ pub(crate) fn build_if_stmt( }, span, source_mode: crate::source::current_parse_mode(), + strict_types: crate::source::current_strict_types(), attributes: Vec::new(), }; } @@ -540,6 +541,7 @@ pub(crate) fn build_if_stmt( }, span, source_mode: crate::source::current_parse_mode(), + strict_types: crate::source::current_strict_types(), attributes: Vec::new(), } } diff --git a/src/optimize/propagate/simulate.rs b/src/optimize/propagate/simulate.rs index f6240d2589..da30f49922 100644 --- a/src/optimize/propagate/simulate.rs +++ b/src/optimize/propagate/simulate.rs @@ -15,6 +15,9 @@ use super::*; /// Intersects multiple constant environments, retaining only variable assignments /// that are identical across every path. Returns an empty map if no paths are provided. /// +/// Agreement is decided by `PropagatedValue::same_constant`, not `PartialEq`, so paths that +/// assign `0.0` and `-0.0` do not merge: `echo` prints `0` for one and `-0` for the other. +/// /// - `paths`: Vector of constant environments from different control-flow paths /// - Returns: A merged environment where each variable must have the same value in all input paths pub(crate) fn merge_constant_env_paths(mut paths: Vec) -> ConstantEnv { @@ -24,7 +27,11 @@ pub(crate) fn merge_constant_env_paths(mut paths: Vec) -> ConstantE first .into_iter() - .filter(|(name, value)| paths.iter().all(|path| path.get(name) == Some(value))) + .filter(|(name, value)| { + paths + .iter() + .all(|path| path.get(name).is_some_and(|known| known.same_constant(value))) + }) .collect() } diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index 092e5eca43..81dc5d30b5 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -111,8 +111,8 @@ pub(crate) fn propagate_block(body: Vec, mut env: ConstantEnv) -> (Vec (Stmt, ConstantEnv) { - let source_mode = stmt.source_mode; - crate::source::with_parse_mode(source_mode, || propagate_stmt_in_source_mode(stmt, env)) + let profile = stmt.profile(); + crate::source::with_parse_mode(profile, || propagate_stmt_in_source_mode(stmt, env)) } /// Propagates one statement while reconstructed nodes inherit its physical source mode. diff --git a/src/optimize/tests/dce/guards.rs b/src/optimize/tests/dce/guards.rs index 0ba601d31e..1e2d0c3ff9 100644 --- a/src/optimize/tests/dce/guards.rs +++ b/src/optimize/tests/dce/guards.rs @@ -13,6 +13,7 @@ use super::*; mod outer_guards; mod excluded_guards; mod composite_guards; +mod float_guards; mod range_guards; mod relational_guards; mod loop_guards; diff --git a/src/optimize/tests/dce/guards/float_guards.rs b/src/optimize/tests/dce/guards/float_guards.rs new file mode 100644 index 0000000000..35d5d7530e --- /dev/null +++ b/src/optimize/tests/dce/guards/float_guards.rs @@ -0,0 +1,138 @@ +//! Purpose: +//! Regression tests pinning DCE guard literals to PHP's `===` semantics for floats. +//! +//! Called from: +//! - `crate::optimize::tests` through Rust's test harness. +//! +//! Key details: +//! - `0.0 === -0.0` is `true` in PHP but the two have different bit patterns, so a guard state +//! keyed on bits prunes a branch the program can actually reach. +//! - The fixtures put the guard literal on the *left* of the inner comparison; with the literal +//! on the right the structural `condition_guards` lookup answers first and hides the bug. + +use super::*; + +/// Builds `function probe($x) { if ($x === outer) { if (inner === $x) { echo 1; } else { echo 2; } } }` +/// and returns the inner if-statement's surviving branches after DCE. +fn eliminate_nested_float_guard(outer: Expr, inner: Expr) -> Vec { + let program = vec![Stmt::new( + StmtKind::FunctionDecl { + name: "probe".into(), + params: vec![("x".to_string(), None, None, false)], + param_attributes: vec![Vec::new()], + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: None, + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::If { + condition: Expr::binop(Expr::var("x"), BinOp::StrictEq, outer), + then_body: vec![Stmt::new( + StmtKind::If { + condition: Expr::binop(inner, BinOp::StrictEq, Expr::var("x")), + then_body: vec![Stmt::echo(Expr::int_lit(1))], + elseif_clauses: Vec::new(), + else_body: Some(vec![Stmt::echo(Expr::int_lit(2))]), + }, + Span::dummy(), + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + Span::dummy(), + )], + }, + Span::dummy(), + )]; + + let eliminated = eliminate_dead_code(program); + let StmtKind::FunctionDecl { body, .. } = &eliminated[0].kind else { + panic!("expected function"); + }; + let StmtKind::If { then_body, .. } = &body[0].kind else { + panic!("expected outer if"); + }; + then_body.clone() +} + +/// Verifies a `=== 0.0` guard does not prune a nested `-0.0 === $x` branch. +/// +/// PHP prints `1` here (`-0.0 === 0.0` is `true`), but a bit-pattern guard comparison decided +/// the inner condition was false and eliminated the reachable branch. +#[test] +fn test_signed_zero_guard_keeps_reachable_branch() { + let then_body = eliminate_nested_float_guard(Expr::float_lit(0.0), Expr::float_lit(-0.0)); + assert_eq!(then_body, vec![Stmt::echo(Expr::int_lit(1))]); +} + +/// Verifies the mirrored fixture: a `=== -0.0` guard proves a nested `0.0 === $x` is true. +#[test] +fn test_negative_zero_guard_keeps_reachable_branch() { + let then_body = eliminate_nested_float_guard(Expr::float_lit(-0.0), Expr::float_lit(0.0)); + assert_eq!(then_body, vec![Stmt::echo(Expr::int_lit(1))]); +} + +/// Verifies a guard on a different float value still prunes the impossible branch. +/// +/// Guards the fix above against over-correcting into "never conclude anything about floats". +#[test] +fn test_distinct_float_guard_still_prunes() { + let then_body = eliminate_nested_float_guard(Expr::float_lit(1.0), Expr::float_lit(2.0)); + assert_eq!(then_body, vec![Stmt::echo(Expr::int_lit(2))]); +} + +/// Verifies a `!== 0.0` exclusion guard rules out a nested `-0.0 === $x`. +/// +/// `-0.0 === 0.0` is `true`, so a value that is provably not `0.0` cannot be `-0.0` either; +/// the fix makes the excluded-value lookup see that through PHP's `===` instead of bits. +#[test] +fn test_signed_zero_exclusion_guard_prunes_impossible_branch() { + let program = vec![Stmt::new( + StmtKind::FunctionDecl { + name: "probe".into(), + params: vec![("x".to_string(), None, None, false)], + param_attributes: vec![Vec::new()], + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: None, + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::If { + condition: Expr::binop( + Expr::var("x"), + BinOp::StrictNotEq, + Expr::float_lit(0.0), + ), + then_body: vec![Stmt::new( + StmtKind::If { + condition: Expr::binop( + Expr::float_lit(-0.0), + BinOp::StrictEq, + Expr::var("x"), + ), + then_body: vec![Stmt::echo(Expr::int_lit(1))], + elseif_clauses: Vec::new(), + else_body: Some(vec![Stmt::echo(Expr::int_lit(2))]), + }, + Span::dummy(), + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + Span::dummy(), + )], + }, + Span::dummy(), + )]; + + let eliminated = eliminate_dead_code(program); + let StmtKind::FunctionDecl { body, .. } = &eliminated[0].kind else { + panic!("expected function"); + }; + let StmtKind::If { then_body, .. } = &body[0].kind else { + panic!("expected outer if"); + }; + assert_eq!(then_body, &vec![Stmt::echo(Expr::int_lit(2))]); +} diff --git a/src/optimize/tests/fold.rs b/src/optimize/tests/fold.rs index bdbdaf2e64..7c7467d5c4 100644 --- a/src/optimize/tests/fold.rs +++ b/src/optimize/tests/fold.rs @@ -10,6 +10,8 @@ use super::*; +mod php_semantics; + /// Verifies fold_constants evaluates (2+3)*4 to 20, respecting AST structure. #[test] fn test_fold_nested_integer_arithmetic() { @@ -37,9 +39,10 @@ fn test_fold_nested_integer_arithmetic() { assert_eq!(folded, vec![Stmt::echo(Expr::int_lit(20))]); } -/// Verifies 2 ** 3 is folded to FloatLiteral(8.0) — exponentiation yields float. +/// Verifies `2 ** 3` folds to `IntLiteral(8)`: PHP keeps an integer result when the base and a +/// non-negative exponent are integers and the result fits (`var_dump(2 ** 3)` is `int(8)`). #[test] -fn test_fold_constant_pow_to_float_literal() { +fn test_fold_constant_pow_to_int_literal() { let program = vec![Stmt::echo(Expr::new( ExprKind::BinaryOp { left: Box::new(Expr::int_lit(2)), @@ -51,13 +54,7 @@ fn test_fold_constant_pow_to_float_literal() { let folded = fold_constants(program); - assert_eq!( - folded, - vec![Stmt::echo(Expr::new( - ExprKind::FloatLiteral(8.0), - Span::dummy(), - ))] - ); + assert_eq!(folded, vec![Stmt::echo(Expr::int_lit(8))]); } /// Verifies division by zero is NOT folded — PHP would fatal, optimizer preserves the AST. @@ -279,9 +276,11 @@ fn test_fold_scalar_casts_when_result_is_unambiguous() { ); } -/// Verifies int("42abc") is NOT folded — ambiguous string casts must stay unfolded. +/// Verifies `(int) "42abc"` folds to `42`, matching PHP's leading-numeric-prefix rule. +/// +/// php -r 'var_dump((int) "42abc");' prints `int(42)`. #[test] -fn test_keep_ambiguous_string_casts_unfolded() { +fn test_fold_leading_numeric_string_cast() { let expr = Expr::new( ExprKind::Cast { target: CastType::Int, @@ -290,9 +289,9 @@ fn test_keep_ambiguous_string_casts_unfolded() { Span::dummy(), ); - let folded = fold_constants(vec![Stmt::echo(expr.clone())]); + let folded = fold_constants(vec![Stmt::echo(expr)]); - assert_eq!(folded, vec![Stmt::echo(expr)]); + assert_eq!(folded, vec![Stmt::echo(Expr::int_lit(42))]); } /// Verifies `$items[0] = 5` result_target is dropped when structurally equal to target. diff --git a/src/optimize/tests/fold/php_semantics.rs b/src/optimize/tests/fold/php_semantics.rs new file mode 100644 index 0000000000..9a2ef97ef0 --- /dev/null +++ b/src/optimize/tests/fold/php_semantics.rs @@ -0,0 +1,728 @@ +//! Purpose: +//! Regression tests pinning constant folding to PHP 8.4's observable results for comparisons, +//! integer arithmetic overflow, array-key normalization, and string casts. +//! +//! Called from: +//! - `crate::optimize::tests` through Rust's test harness. +//! +//! Key details: +//! - Every expectation in this file was produced by running the equivalent snippet under +//! `php -r` on PHP 8.4.20; the table in `test_fold_comparisons_match_php` is generated from +//! a `<=> / == / < / > / <= / >=` sweep over the same operand set. +//! - `fold_constants` is driven end-to-end rather than the private helpers, so the tests cover +//! the operand-order rules relational folding depends on. + +use super::*; + +/// Builds the literal expression named by the comparison table's operand keys. +/// +/// The names mirror the PHP fixture that generated the expected results, so a row can be read +/// back against `php -r` verbatim. +fn comparison_operand(name: &str) -> Expr { + match name { + "null" => Expr::new(ExprKind::Null, Span::dummy()), + "false" => Expr::new(ExprKind::BoolLiteral(false), Span::dummy()), + "true" => Expr::new(ExprKind::BoolLiteral(true), Span::dummy()), + "i0" => Expr::int_lit(0), + "i1" => Expr::int_lit(1), + "i2" => Expr::int_lit(2), + "im1" => Expr::int_lit(-1), + "intmax" => Expr::int_lit(i64::MAX), + "intmax_m1" => Expr::int_lit(i64::MAX - 1), + "intmin" => Expr::int_lit(i64::MIN), + "f0" => Expr::float_lit(0.0), + "fneg0" => Expr::float_lit(-0.0), + "f1" => Expr::float_lit(1.0), + "f1_5" => Expr::float_lit(1.5), + "fbig" => Expr::float_lit(9.2233720368547758e18), + "s_empty" => Expr::string_lit(""), + "s_0" => Expr::string_lit("0"), + "s_1" => Expr::string_lit("1"), + "s_01" => Expr::string_lit("01"), + "s_sp1" => Expr::string_lit(" 1"), + "s_1sp" => Expr::string_lit("1 "), + "s_1e1" => Expr::string_lit("1e1"), + "s_10" => Expr::string_lit("10"), + "s_0e1" => Expr::string_lit("0e1"), + "s_0e2" => Expr::string_lit("0e2"), + "s_abc" => Expr::string_lit("abc"), + "s_1abc" => Expr::string_lit("1abc"), + "s_spaces" => Expr::string_lit(" "), + "s_intmax_m1" => Expr::string_lit("9223372036854775806"), + "s_intmax" => Expr::string_lit("9223372036854775807"), + "s_over1" => Expr::string_lit("9223372036854775808"), + "s_over2" => Expr::string_lit("9223372036854775809"), + "s_intmin" => Expr::string_lit("-9223372036854775808"), + "s_1_5" => Expr::string_lit("1.5"), + "s_a" => Expr::string_lit("a"), + "s_b" => Expr::string_lit("b"), + "s_A" => Expr::string_lit("A"), + other => panic!("unknown comparison operand {other}"), + } +} + +/// Folds `left op right` and returns the resulting expression kind. +fn fold_binop(left: Expr, op: BinOp, right: Expr) -> ExprKind { + let folded = fold_constants(vec![Stmt::echo(Expr::binop(left, op, right))]); + let StmtKind::Echo(expr) = &folded[0].kind else { + panic!("expected echo statement"); + }; + expr.kind.clone() +} + +/// Folds `left op right` where both operands are looked up from the comparison table names. +fn fold_named_binop(left: &str, op: BinOp, right: &str) -> ExprKind { + fold_binop(comparison_operand(left), op, comparison_operand(right)) +} + +/// PHP 8.4 results for `<=>`, `==`, `<`, `>`, `<=` and `>=` over operand pairs that exercise +/// integer precision, numeric-string classification, and the null/bool fallbacks. +#[rustfmt::skip] +const PHP_COMPARISONS: &[(&str, &str, i64, bool, bool, bool, bool, bool)] = &[ + ("intmax_m1", "intmax", -1, false, true, false, true, false), + ("intmax", "intmax_m1", 1, false, false, true, false, true), + ("intmax", "intmax", 0, true, false, false, true, true), + ("intmax_m1", "s_intmax", -1, false, true, false, true, false), + ("s_intmax_m1", "s_intmax", -1, false, true, false, true, false), + ("s_intmax_m1", "intmax", -1, false, true, false, true, false), + ("intmax", "fbig", 0, true, false, false, true, true), + ("intmax_m1", "fbig", 0, true, false, false, true, true), + ("fbig", "intmax", 0, true, false, false, true, true), + ("intmax_m1", "s_over1", 0, true, false, false, true, true), + ("s_over1", "s_over2", -1, false, true, false, true, false), + ("intmin", "s_intmin", 0, true, false, false, true, true), + ("s_0e1", "s_0e2", 0, true, false, false, true, true), + ("s_1e1", "s_10", 0, true, false, false, true, true), + ("s_10", "s_1e1", 0, true, false, false, true, true), + ("s_1", "s_01", 0, true, false, false, true, true), + ("s_01", "s_1", 0, true, false, false, true, true), + ("s_sp1", "i1", 0, true, false, false, true, true), + ("s_1sp", "i1", 0, true, false, false, true, true), + ("s_1", "i1", 0, true, false, false, true, true), + ("s_abc", "i0", 1, false, false, true, false, true), + ("i0", "s_abc", -1, false, true, false, true, false), + ("s_1abc", "i1", 1, false, false, true, false, true), + ("s_empty", "i0", -1, false, true, false, true, false), + ("i0", "s_empty", 1, false, false, true, false, true), + ("s_spaces", "i0", -1, false, true, false, true, false), + ("null", "s_empty", 0, true, false, false, true, true), + ("null", "s_0", -1, false, true, false, true, false), + ("null", "i0", 0, true, false, false, true, true), + ("null", "false", 0, true, false, false, true, true), + ("null", "true", -1, false, true, false, true, false), + ("false", "s_0", 0, true, false, false, true, true), + ("true", "i1", 0, true, false, false, true, true), + ("true", "s_abc", 0, true, false, false, true, true), + ("false", "s_empty", 0, true, false, false, true, true), + ("i0", "null", 0, true, false, false, true, true), + ("s_0", "false", 0, true, false, false, true, true), + ("f0", "fneg0", 0, true, false, false, true, true), + ("fneg0", "f0", 0, true, false, false, true, true), + ("f1_5", "s_1_5", 0, true, false, false, true, true), + ("s_1_5", "f1_5", 0, true, false, false, true, true), + ("f1", "i1", 0, true, false, false, true, true), + ("f1", "s_1", 0, true, false, false, true, true), + ("s_a", "s_b", -1, false, true, false, true, false), + ("s_b", "s_a", 1, false, false, true, false, true), + ("s_A", "s_a", -1, false, true, false, true, false), + ("s_abc", "s_abc", 0, true, false, false, true, true), + ("im1", "true", 0, true, false, false, true, true), + ("i2", "true", 0, true, false, false, true, true), + ("s_a", "true", 0, true, false, false, true, true), + ("i0", "false", 0, true, false, false, true, true), +]; + +/// Verifies that every folded comparison matches PHP 8.4. +/// +/// The regression this pins is B5: `PHP_INT_MAX - 1 <=> PHP_INT_MAX` used to fold through +/// `f64` and answer `0`, and `PHP_INT_MAX - 1 == "9223372036854775807"` used to answer `true`. +#[test] +fn test_fold_comparisons_match_php() { + for &(left, right, spaceship, eq, lt, gt, le, ge) in PHP_COMPARISONS { + assert_eq!( + fold_named_binop(left, BinOp::Spaceship, right), + ExprKind::IntLiteral(spaceship), + "{left} <=> {right}" + ); + for (op, expected, label) in [ + (BinOp::Eq, eq, "=="), + (BinOp::NotEq, !eq, "!="), + (BinOp::Lt, lt, "<"), + (BinOp::Gt, gt, ">"), + (BinOp::LtEq, le, "<="), + (BinOp::GtEq, ge, ">="), + ] { + assert_eq!( + fold_named_binop(left, op, right), + ExprKind::BoolLiteral(expected), + "{left} {label} {right}" + ); + } + } +} + +/// Operand set for the string-versus-string `<=>` matrix below. +#[rustfmt::skip] +const NUMERIC_STRING_OPERANDS: &[&str] = &[ + "", "0", "1", "01", " 1", "1 ", "1e1", "10", "0e1", "abc", "1abc", "1.5", ".5", + "5.", "1e400", "-1e400", "9223372036854775807", "9223372036854775808", + "-9223372036854775808", "-9223372036854775809", " ", "0x1A", "1_000", "007", "+1", +]; + +/// PHP 8.4 `$a <=> $b` for every pair of `NUMERIC_STRING_OPERANDS`, row-major, encoded as +/// `<` / `=` / `>`. Generated with a `foreach` sweep under `php -r`. +/// +/// This is `zendi_smart_strcmp` coverage: two integer strings compare as integers, two +/// same-side overflowed integers or two infinities fall back to a byte comparison, and any +/// non-numeric operand makes the whole comparison byte-wise. +#[rustfmt::skip] +const PHP_STRING_ORDERINGS: &[&str] = &[ + "=<<<<<<<<<<<<<<<<<<<<<<<<", + ">=<<<<<<=<<<<<<><<>>><<<<", + ">>====<<><<<><<><<>>>><<=", + ">>====<<><<<><<><<>>><<<=", + ">>====<<><<<><<><<>>><<<=", + ">>====<<><<<><<><<>>>><<=", + ">>>>>>==><>>>><><<>>>>>>>", + ">>>>>>==><<>>><><<>>>><>>", + ">=<<<<<<=<<<<<<><<>>><<<<", + ">>>>>>>>>=>>>>>>>>>>>>>>>", + ">>>>>><>><=>><<><<>>>>>>>", + ">>>>>><<><<=><<><<>>>><<>", + ">><<<<<<><<<=<<><<>>><<<<", + ">>>>>><<><>>>=<><<>>>>><>", + ">>>>>>>>><>>>>=>>>>>>>>>>", + "><<<<<<<<<<<<<<=<<<<><<<<", + ">>>>>>>>><>>>><>=<>>>>>>>", + ">>>>>>>>><>>>><>>=>>>>>>>", + "><<<<<<<<<<<<<<><<=>><<<<", + "><<<<<<<<<<<<<<><<<=><<<<", + "><<<<<<<<<<<<<<<<<<<=<<<<", + ">><>><<<><<<><<><<>>>=<>>", + ">>>>>><>><<>><<><<>>>>=>>", + ">>>>>><<><<>>><><<>>><<=>", + ">>====<<><<<><<><<>>><<<=", +]; + +/// Verifies string-versus-string `<=>` folding reproduces PHP's smart string comparison. +/// +/// Before the fix both operands were parsed to `f64`, so `"9223372036854775807"` and +/// `"9223372036854775808"` compared equal. +#[test] +fn test_fold_string_orderings_match_php() { + assert_eq!(NUMERIC_STRING_OPERANDS.len(), PHP_STRING_ORDERINGS.len()); + for (left, row) in NUMERIC_STRING_OPERANDS.iter().zip(PHP_STRING_ORDERINGS) { + assert_eq!(row.len(), NUMERIC_STRING_OPERANDS.len()); + for (right, expected) in NUMERIC_STRING_OPERANDS.iter().zip(row.chars()) { + let expected = match expected { + '<' => -1, + '=' => 0, + '>' => 1, + other => panic!("bad expectation {other}"), + }; + assert_eq!( + fold_binop( + Expr::string_lit(*left), + BinOp::Spaceship, + Expr::string_lit(*right) + ), + ExprKind::IntLiteral(expected), + "{left:?} <=> {right:?}" + ); + } + } +} + +/// Verifies NAN keeps PHP's asymmetric comparison behavior. +/// +/// PHP answers `false` for `NAN < 1`, `NAN > 1`, `NAN <= 1` and `NAN >= 1` alike, but `1 <=> +/// NAN` and `NAN <=> NAN` are both `1`, because `zend_compare` returns `1` for any NAN pair +/// and the relational operators are spelled through it in a fixed argument order. +#[test] +fn test_fold_nan_comparisons_match_php() { + let nan = || Expr::float_lit(f64::NAN); + for op in [BinOp::Lt, BinOp::Gt, BinOp::LtEq, BinOp::GtEq] { + assert_eq!( + fold_binop(nan(), op.clone(), Expr::int_lit(1)), + ExprKind::BoolLiteral(false), + "NAN {op:?} 1" + ); + assert_eq!( + fold_binop(Expr::int_lit(1), op.clone(), nan()), + ExprKind::BoolLiteral(false), + "1 {op:?} NAN" + ); + } + assert_eq!( + fold_binop(Expr::int_lit(1), BinOp::Spaceship, nan()), + ExprKind::IntLiteral(1) + ); + assert_eq!( + fold_binop(nan(), BinOp::Spaceship, nan()), + ExprKind::IntLiteral(1) + ); + assert_eq!( + fold_binop(nan(), BinOp::Eq, nan()), + ExprKind::BoolLiteral(false) + ); +} + +/// Verifies a float against a non-numeric string is left for the runtime. +/// +/// PHP stringifies the float through `zend_double_to_str` and compares bytes, so `INF == +/// "INF"` is `true`; the fold refuses to guess that formatting and keeps the operation. +#[test] +fn test_float_versus_non_numeric_string_declines_fold() { + let expr = Expr::binop(Expr::float_lit(1.5), BinOp::Eq, Expr::string_lit("abc")); + let folded = fold_constants(vec![Stmt::echo(expr.clone())]); + assert_eq!(folded, vec![Stmt::echo(expr)]); +} + +/// Verifies integer arithmetic folds match PHP, including every overflow boundary. +/// +/// `PHP_INT_MIN % -1` used to panic the compiler with "attempt to calculate the remainder +/// with overflow"; `6 / 3` and `2 ** 3` used to fold to floats; `1 << 64` and `-PHP_INT_MIN` +/// used to decline and reach a wrapping runtime. +#[test] +fn test_fold_integer_arithmetic_matches_php() { + // php -r 'var_dump(PHP_INT_MIN % -1, 7 % 3, -7 % 3, 7 % -3);' + assert_eq!( + fold_binop(Expr::int_lit(i64::MIN), BinOp::Mod, Expr::int_lit(-1)), + ExprKind::IntLiteral(0) + ); + assert_eq!( + fold_binop(Expr::int_lit(-7), BinOp::Mod, Expr::int_lit(3)), + ExprKind::IntLiteral(-1) + ); + + // php -r 'var_dump(6 / 3, 7 / 2, PHP_INT_MIN / -1);' + assert_eq!( + fold_binop(Expr::int_lit(6), BinOp::Div, Expr::int_lit(3)), + ExprKind::IntLiteral(2) + ); + assert_eq!( + fold_binop(Expr::int_lit(7), BinOp::Div, Expr::int_lit(2)), + ExprKind::FloatLiteral(3.5) + ); + assert_eq!( + fold_binop(Expr::int_lit(i64::MIN), BinOp::Div, Expr::int_lit(-1)), + ExprKind::FloatLiteral(9.223372036854776e18) + ); + + // php -r 'var_dump(2 ** 3, (-2) ** 3, 2 ** 0, 2 ** -1);' + assert_eq!( + fold_binop(Expr::int_lit(2), BinOp::Pow, Expr::int_lit(3)), + ExprKind::IntLiteral(8) + ); + assert_eq!( + fold_binop(Expr::int_lit(-2), BinOp::Pow, Expr::int_lit(3)), + ExprKind::IntLiteral(-8) + ); + assert_eq!( + fold_binop(Expr::int_lit(2), BinOp::Pow, Expr::int_lit(0)), + ExprKind::IntLiteral(1) + ); + assert_eq!( + fold_binop(Expr::int_lit(2), BinOp::Pow, Expr::int_lit(-1)), + ExprKind::FloatLiteral(0.5) + ); + + // Overflowing `**` follows PHP's square-and-multiply loop, not a single `pow()` call: the + // two differ in the last ULP for most inputs. + // php -r 'printf("%.17g %.17g %.17g", 2 ** 64, 654 ** 32, (-133) ** 101);' + assert_eq!( + fold_binop(Expr::int_lit(2), BinOp::Pow, Expr::int_lit(64)), + ExprKind::FloatLiteral(1.8446744073709552e19) + ); + assert_eq!( + fold_binop(Expr::int_lit(654), BinOp::Pow, Expr::int_lit(32)), + ExprKind::FloatLiteral(1.2545499179770422e90) + ); + assert_eq!( + fold_binop(Expr::int_lit(-133), BinOp::Pow, Expr::int_lit(101)), + ExprKind::FloatLiteral(-3.2286111158631344e214) + ); + + // php -r 'var_dump(1 << 63, 1 << 64, -1 >> 64, 8 >> 64, -1 >> 63);' + assert_eq!( + fold_binop(Expr::int_lit(1), BinOp::ShiftLeft, Expr::int_lit(63)), + ExprKind::IntLiteral(i64::MIN) + ); + assert_eq!( + fold_binop(Expr::int_lit(1), BinOp::ShiftLeft, Expr::int_lit(64)), + ExprKind::IntLiteral(0) + ); + assert_eq!( + fold_binop(Expr::int_lit(-1), BinOp::ShiftRight, Expr::int_lit(64)), + ExprKind::IntLiteral(-1) + ); + assert_eq!( + fold_binop(Expr::int_lit(8), BinOp::ShiftRight, Expr::int_lit(64)), + ExprKind::IntLiteral(0) + ); + + // php -r 'var_dump(PHP_INT_MAX + 1, PHP_INT_MIN - 1, PHP_INT_MAX * 2);' + assert_eq!( + fold_binop(Expr::int_lit(i64::MAX), BinOp::Add, Expr::int_lit(1)), + ExprKind::FloatLiteral(9.223372036854776e18) + ); +} + +/// Verifies a negative shift count is not folded so the runtime raises `ArithmeticError`. +#[test] +fn test_negative_shift_declines_fold() { + for op in [BinOp::ShiftLeft, BinOp::ShiftRight] { + let expr = Expr::binop(Expr::int_lit(1), op, Expr::int_lit(-1)); + let folded = fold_constants(vec![Stmt::echo(expr.clone())]); + assert_eq!(folded, vec![Stmt::echo(expr)]); + } +} + +/// Verifies `-PHP_INT_MIN` folds to the float PHP produces instead of wrapping to `PHP_INT_MIN`. +/// +/// php -r 'var_dump(-PHP_INT_MIN);' prints `float(9.2233720368548E+18)`. +#[test] +fn test_fold_negate_int_min_promotes_to_float() { + let folded = fold_constants(vec![Stmt::echo(Expr::new( + ExprKind::Negate(Box::new(Expr::int_lit(i64::MIN))), + Span::dummy(), + ))]); + let StmtKind::Echo(expr) = &folded[0].kind else { + panic!("expected echo statement"); + }; + assert_eq!(expr.kind, ExprKind::FloatLiteral(9.223372036854776e18)); +} + +/// Builds an associative array literal access `[key => value, ...][index]`. +fn assoc_access(entries: Vec<(Expr, Expr)>, index: Expr) -> Expr { + Expr::new( + ExprKind::ArrayAccess { + array: Box::new(Expr::new( + ExprKind::ArrayLiteralAssoc(entries), + Span::dummy(), + )), + index: Box::new(index), + }, + Span::dummy(), + ) +} + +/// Verifies associative array-literal access normalizes keys the way PHP's hash table does. +/// +/// Before the fix the fold compared raw scalar variants, so `[0 => "a", false => "b"][0]` +/// folded to `"a"` while PHP prints `"b"` — the two keys are the same slot and the literal is +/// built last-wins. +#[test] +fn test_fold_assoc_access_normalizes_php_array_keys() { + let null = || Expr::new(ExprKind::Null, Span::dummy()); + let bool_lit = |value| Expr::new(ExprKind::BoolLiteral(value), Span::dummy()); + + // php -r 'var_dump([0 => "a", false => "b"][0]);' → "b" + let cases: Vec<(Vec<(Expr, Expr)>, Expr, &str)> = vec![ + ( + vec![ + (Expr::int_lit(0), Expr::string_lit("a")), + (bool_lit(false), Expr::string_lit("b")), + ], + Expr::int_lit(0), + "b", + ), + ( + vec![ + (Expr::string_lit("1"), Expr::string_lit("a")), + (Expr::int_lit(1), Expr::string_lit("b")), + ], + Expr::string_lit("1"), + "b", + ), + ( + vec![ + (null(), Expr::string_lit("a")), + (Expr::string_lit(""), Expr::string_lit("b")), + ], + null(), + "b", + ), + ( + vec![ + (bool_lit(true), Expr::string_lit("a")), + (Expr::int_lit(1), Expr::string_lit("b")), + ], + bool_lit(true), + "b", + ), + // php -r 'var_dump(["01" => "a", 1 => "b"]["01"]);' → "a": "01" is not an integer key. + ( + vec![ + (Expr::string_lit("01"), Expr::string_lit("a")), + (Expr::int_lit(1), Expr::string_lit("b")), + ], + Expr::string_lit("01"), + "a", + ), + // php -r 'var_dump([" 1" => "a", 1 => "b"][" 1"]);' → "a": leading space keeps a string key. + ( + vec![ + (Expr::string_lit(" 1"), Expr::string_lit("a")), + (Expr::int_lit(1), Expr::string_lit("b")), + ], + Expr::string_lit(" 1"), + "a", + ), + // php -r 'var_dump([2.0 => "a", 2 => "b"][2]);' → "b": integral floats truncate silently. + ( + vec![ + (Expr::float_lit(2.0), Expr::string_lit("a")), + (Expr::int_lit(2), Expr::string_lit("b")), + ], + Expr::int_lit(2), + "b", + ), + ]; + + for (entries, index, expected) in cases { + let folded = fold_constants(vec![Stmt::echo(assoc_access(entries, index))]); + assert_eq!(folded, vec![Stmt::echo(Expr::string_lit(expected))]); + } +} + +/// Verifies a lossy float array key is not folded so the runtime keeps PHP's deprecation. +/// +/// php -r 'var_dump([1.7 => "a"][1]);' emits "Implicit conversion from float 1.7 to int loses +/// precision" before printing `"a"`. +#[test] +fn test_lossy_float_array_key_declines_fold() { + let expr = assoc_access( + vec![(Expr::float_lit(1.7), Expr::string_lit("a"))], + Expr::int_lit(1), + ); + let folded = fold_constants(vec![Stmt::echo(expr.clone())]); + assert_eq!(folded, vec![Stmt::echo(expr)]); +} + +/// Verifies indexed array-literal access normalizes the index like PHP. +/// +/// php -r 'var_dump(["a", "b"][true], ["a", "b"]["1"]);' prints `"b"` twice. +#[test] +fn test_fold_indexed_access_normalizes_index() { + let items = || { + Expr::new( + ExprKind::ArrayLiteral(vec![Expr::string_lit("a"), Expr::string_lit("b")]), + Span::dummy(), + ) + }; + for index in [ + Expr::new(ExprKind::BoolLiteral(true), Span::dummy()), + Expr::string_lit("1"), + Expr::int_lit(1), + ] { + let folded = fold_constants(vec![Stmt::echo(Expr::new( + ExprKind::ArrayAccess { + array: Box::new(items()), + index: Box::new(index), + }, + Span::dummy(), + ))]); + assert_eq!(folded, vec![Stmt::echo(Expr::string_lit("b"))]); + } +} + +/// Folds `(target) expr` and returns the resulting expression kind. +fn fold_cast(target: CastType, expr: Expr) -> ExprKind { + let folded = fold_constants(vec![Stmt::echo(Expr::new( + ExprKind::Cast { + target, + expr: Box::new(expr), + }, + Span::dummy(), + ))]); + let StmtKind::Echo(expr) = &folded[0].kind else { + panic!("expected echo statement"); + }; + expr.kind.clone() +} + +/// PHP 8.4 results for `(float)` and `(int)` casts of string literals. +/// +/// Produced by `php -r 'printf("%s %s", var_export((float) $s, true), var_export((int) $s, true));'` +/// for each subject. +#[rustfmt::skip] +const PHP_STRING_CASTS: &[(&str, f64, i64)] = &[ + // Rust's `f64` parser accepts these; PHP's numeric grammar does not. + ("INF", 0.0, 0), + ("inf", 0.0, 0), + ("nan", 0.0, 0), + ("NaN", 0.0, 0), + ("infinity", 0.0, 0), + ("-INF", 0.0, 0), + // Prefix parsing. + ("1e3", 1000.0, 1000), + (" 12", 12.0, 12), + ("12 ", 12.0, 12), + ("\n12", 12.0, 12), + ("12abc", 12.0, 12), + (" -12xyz", -12.0, -12), + ("0x1A", 0.0, 0), + ("0b101", 0.0, 0), + ("1_000", 1.0, 1), + (".5", 0.5, 0), + ("5.", 5.0, 5), + ("+.5e-2", 0.005, 0), + ("1.2.3", 1.2, 1), + ("1e", 1.0, 1), + ("1e+", 1.0, 1), + ("007", 7.0, 7), + // No numeric prefix at all. + ("abc", 0.0, 0), + ("", 0.0, 0), + ("-", 0.0, 0), + ("- 1", 0.0, 0), + (".", 0.0, 0), + ("--1", 0.0, 0), + // Saturation and range. + ("9223372036854775807", 9.223372036854776e18, i64::MAX), + ("9223372036854775808", 9.223372036854776e18, i64::MAX), + ("-9223372036854775808", -9.223372036854776e18, i64::MIN), + ("-9223372036854775809", -9.223372036854776e18, i64::MIN), + ("1e-400", 0.0, 0), +]; + +/// Verifies `(float)` and `(int)` string casts fold to PHP's results. +/// +/// The regression this pins is B14: the fold used Rust's `str::parse::()`, so +/// `(float) "INF"` folded to infinity and `(float) "nan"` to NAN, where PHP produces `0`. +#[test] +fn test_fold_string_casts_match_php() { + for &(subject, expected_float, expected_int) in PHP_STRING_CASTS { + assert_eq!( + fold_cast(CastType::Float, Expr::string_lit(subject)), + ExprKind::FloatLiteral(expected_float), + "(float) {subject:?}" + ); + assert_eq!( + fold_cast(CastType::Int, Expr::string_lit(subject)), + ExprKind::IntLiteral(expected_int), + "(int) {subject:?}" + ); + } + // php -r 'var_dump((float) "1e400");' → float(INF) + let ExprKind::FloatLiteral(value) = fold_cast(CastType::Float, Expr::string_lit("1e400")) + else { + panic!("expected folded float literal"); + }; + assert!(value.is_infinite() && value.is_sign_positive()); + // php -r 'var_dump((int) "1e400");' → int(0): `zend_dval_to_lval_cap` zeroes non-finite input. + assert_eq!( + fold_cast(CastType::Int, Expr::string_lit("1e400")), + ExprKind::IntLiteral(0) + ); +} + +/// Verifies a ternary whose arms are `0.0` and `-0.0` is not collapsed to one constant. +/// +/// PHP prints `-0` for `echo -0.0`, so the sign is observable: propagating `0.0` into a use of +/// a variable that may hold `-0.0` changes the program's output. +#[test] +fn test_signed_zero_ternary_arms_do_not_merge() { + let program = vec![ + Stmt::assign( + "x", + Expr::new( + ExprKind::Ternary { + condition: Box::new(Expr::var("flag")), + then_expr: Box::new(Expr::float_lit(0.0)), + else_expr: Box::new(Expr::float_lit(-0.0)), + }, + Span::dummy(), + ), + ), + Stmt::echo(Expr::var("x")), + ]; + + let propagated = propagate_constants(program); + + let StmtKind::Echo(expr) = &propagated[1].kind else { + panic!("expected echo statement"); + }; + assert_eq!(expr.kind, ExprKind::Variable("x".to_string())); +} + +/// Verifies an `if`/`else` that assigns `0.0` on one path and `-0.0` on the other does not +/// merge into a single propagated constant. +/// +/// php -r 'if ($argc > 1000) { $x = 0.0; } else { $x = -0.0; } echo $x;' prints `-0`. +#[test] +fn test_signed_zero_branch_assignments_do_not_merge() { + let program = vec![ + Stmt::new( + StmtKind::If { + condition: Expr::var("flag"), + then_body: vec![Stmt::assign("x", Expr::float_lit(0.0))], + elseif_clauses: Vec::new(), + else_body: Some(vec![Stmt::assign("x", Expr::float_lit(-0.0))]), + }, + Span::dummy(), + ), + Stmt::echo(Expr::var("x")), + ]; + + let propagated = propagate_constants(program); + + let StmtKind::Echo(expr) = &propagated[1].kind else { + panic!("expected echo statement"); + }; + assert_eq!(expr.kind, ExprKind::Variable("x".to_string())); +} + +/// Verifies an `if`/`else` that assigns the same float on both paths still merges. +#[test] +fn test_identical_float_branch_assignments_still_merge() { + let program = vec![ + Stmt::new( + StmtKind::If { + condition: Expr::var("flag"), + then_body: vec![Stmt::assign("x", Expr::float_lit(2.5))], + elseif_clauses: Vec::new(), + else_body: Some(vec![Stmt::assign("x", Expr::float_lit(2.5))]), + }, + Span::dummy(), + ), + Stmt::echo(Expr::var("x")), + ]; + + let propagated = propagate_constants(program); + + let StmtKind::Echo(expr) = &propagated[1].kind else { + panic!("expected echo statement"); + }; + assert_eq!(expr.kind, ExprKind::FloatLiteral(2.5)); +} + +/// Verifies a ternary whose arms are the same float constant still merges. +/// +/// Guards the fix above against over-correcting: only the signed-zero (and NAN payload) cases +/// must stay distinct. +#[test] +fn test_identical_float_ternary_arms_still_merge() { + let program = vec![ + Stmt::assign( + "x", + Expr::new( + ExprKind::Ternary { + condition: Box::new(Expr::var("flag")), + then_expr: Box::new(Expr::float_lit(2.5)), + else_expr: Box::new(Expr::float_lit(2.5)), + }, + Span::dummy(), + ), + ), + Stmt::echo(Expr::var("x")), + ]; + + let propagated = propagate_constants(program); + + let StmtKind::Echo(expr) = &propagated[1].kind else { + panic!("expected echo statement"); + }; + assert_eq!(expr.kind, ExprKind::FloatLiteral(2.5)); +} diff --git a/src/optimize/tests/propagate/collections.rs b/src/optimize/tests/propagate/collections.rs index 1774c39949..14a78fe3e8 100644 --- a/src/optimize/tests/propagate/collections.rs +++ b/src/optimize/tests/propagate/collections.rs @@ -12,7 +12,7 @@ use super::*; /// Tests that constant propagation tracks scalar values unpacked from a `list()` assignment. /// The `base` and `exp` variables are initialized from a fixed array literal `[2, 3]`. -/// After propagation, the subsequent `echo $base ** $exp` expression is folded to `8.0`. +/// After propagation, the subsequent `echo $base ** $exp` expression is folded to `8`. #[test] fn test_propagate_constants_tracks_scalar_list_unpack() { let program = vec![ @@ -33,13 +33,13 @@ fn test_propagate_constants_tracks_scalar_list_unpack() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that constant propagation tracks scalar values accessed from a numeric-indexed array literal. /// `$base` is assigned `&$arr[0]` where `$arr = [2, 9]`; after propagation `$base = 2`. -/// The subsequent `echo $base ** 3` is folded to `8.0`. +/// The subsequent `echo $base ** 3` is folded to `8`. #[test] fn test_propagate_constants_tracks_scalar_array_literal_access() { let program = vec![ @@ -64,13 +64,13 @@ fn test_propagate_constants_tracks_scalar_array_literal_access() { assert_eq!(propagated[0], Stmt::assign("base", Expr::int_lit(2))); assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that constant propagation tracks scalar values accessed from an associative array literal. /// `$base` is assigned `&$arr["left"]` where `$arr = ["left" => 2, "right" => 9]`; after propagation `$base = 2`. -/// The subsequent `echo $base ** 3` is folded to `8.0`. +/// The subsequent `echo $base ** 3` is folded to `8`. #[test] fn test_propagate_constants_tracks_scalar_assoc_array_literal_access() { let program = vec![ @@ -98,13 +98,13 @@ fn test_propagate_constants_tracks_scalar_assoc_array_literal_access() { assert_eq!(propagated[0], Stmt::assign("base", Expr::int_lit(2))); assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that constant propagation preserves scalar values that are not targeted by `unset()`. /// `$base = 2` and `$tmp = 9`; `unset($tmp)` invalidates `$tmp` but `$base` remains a constant. -/// After propagation, `echo $base ** 3` is folded to `8.0` while `echo $tmp` is unaffected. +/// After propagation, `echo $base ** 3` is folded to `8` while `echo $tmp` is unaffected. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_unset() { let program = vec![ @@ -127,13 +127,13 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_unset() { assert_eq!( propagated[3], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that `unset()` with multiple targets correctly invalidates all named variables. /// `$base = 2`, `$tmp = 9`, `$other = 10`; `unset($tmp, $other)` invalidates `$tmp` and `$other`. -/// After propagation, `echo $tmp` remains a variable (not folded) and `echo $base ** 3` is `8.0`. +/// After propagation, `echo $tmp` remains a variable (not folded) and `echo $base ** 3` is `8`. #[test] fn test_propagate_constants_invalidates_multiple_unset_targets() { let program = vec![ @@ -159,7 +159,7 @@ fn test_propagate_constants_invalidates_multiple_unset_targets() { assert_eq!(propagated[4], Stmt::echo(Expr::var("tmp"))); assert_eq!( propagated[5], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/propagate/control_paths.rs b/src/optimize/tests/propagate/control_paths.rs index c40d590a9f..2b1cb80f59 100644 --- a/src/optimize/tests/propagate/control_paths.rs +++ b/src/optimize/tests/propagate/control_paths.rs @@ -40,7 +40,7 @@ fn test_propagate_constants_merges_identical_switch_assignments() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -72,7 +72,7 @@ fn test_propagate_constants_merges_identical_try_catch_assignments() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -104,6 +104,6 @@ fn test_propagate_constants_ignores_unreachable_catch_after_non_throwing_try() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/propagate/loops/for_loops.rs b/src/optimize/tests/propagate/loops/for_loops.rs index 0ad5a72fea..fd28318141 100644 --- a/src/optimize/tests/propagate/loops/for_loops.rs +++ b/src/optimize/tests/propagate/loops/for_loops.rs @@ -13,7 +13,7 @@ use super::*; /// Verifies the propagate constants pass can track a variable assigned before a switch, /// resolve the switch subject to its known value, follow the matching case branch /// assignments, and constant-fold a subsequent expression using the propagated result. -/// The echo `base ^ 3` where `base = 2` from the matched case should fold to `8.0`. +/// The echo `base ^ 3` where `base = 2` from the matched case should fold to `8`. #[test] fn test_propagate_constants_uses_known_switch_subject_for_merge() { let program = vec![ @@ -39,13 +39,13 @@ fn test_propagate_constants_uses_known_switch_subject_for_merge() { assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Verifies the pass preserves a scalar variable (`base`) assigned before a for loop /// when the variable is not modified inside the loop body. After the loop, -/// `base ^ 3` should constant-fold to `8.0` even though the loop itself runs. +/// `base ^ 3` should constant-fold to `8` even though the loop itself runs. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_for_loop() { let program = vec![ @@ -72,13 +72,13 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_for_loop() { assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Verifies the pass tracks an assignment (`base = 2`) that occurs inside an infinite /// for loop (no condition) when that assignment is followed by a unconditional break. -/// The variable should be available after the loop for constant folding (`base ^ 3` → `8.0`). +/// The variable should be available after the loop for constant folding (`base ^ 3` → `8`). #[test] fn test_propagate_constants_tracks_assignment_through_for_infinite_break() { let program = vec![ @@ -101,13 +101,13 @@ fn test_propagate_constants_tracks_assignment_through_for_infinite_break() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Verifies the pass tracks assignments from the for loop's init clause even when the /// condition is a constant `false` (so the loop body never executes). The init -/// assignment `base = 2` should still be available for `base ^ 3` → `8.0` outside the loop. +/// assignment `base = 2` should still be available for `base ^ 3` → `8` outside the loop. #[test] fn test_propagate_constants_preserves_for_init_when_condition_is_false() { let program = vec![ @@ -127,13 +127,13 @@ fn test_propagate_constants_preserves_for_init_when_condition_is_false() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Verifies the pass correctly handles stable init-clause assignments: `exp = 3` in the /// for init is not modified within the loop body, so it remains a known constant. -/// Both the echo inside the loop (`base ^ exp` → `8.0`) and the final echo (`exp` → `3`) +/// Both the echo inside the loop (`base ^ exp` → `8`) and the final echo (`exp` → `3`) /// should constant-fold correctly. #[test] fn test_propagate_constants_tracks_stable_for_init_assignments() { @@ -170,7 +170,7 @@ fn test_propagate_constants_tracks_stable_for_init_assignments() { assert_eq!( body[0], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); assert_eq!( propagated[3], diff --git a/src/optimize/tests/propagate/loops/foreach_loops.rs b/src/optimize/tests/propagate/loops/foreach_loops.rs index 9e5feae488..4dfc59ca23 100644 --- a/src/optimize/tests/propagate/loops/foreach_loops.rs +++ b/src/optimize/tests/propagate/loops/foreach_loops.rs @@ -15,7 +15,7 @@ use super::*; /// modified inside the loop. /// /// After the optimizer runs, `$base = 2` followed by a foreach over `[1, 2, 3]` -/// and `echo $base ** 3` should reduce to `echo 8.0` since `$base` is never +/// and `echo $base ** 3` should reduce to `echo 8` since `$base` is never /// reassigned in the loop body. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_foreach_loop() { @@ -45,6 +45,6 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_foreach_loop() { assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/propagate/loops/loop_state.rs b/src/optimize/tests/propagate/loops/loop_state.rs index d57af336d1..fd26df714c 100644 --- a/src/optimize/tests/propagate/loops/loop_state.rs +++ b/src/optimize/tests/propagate/loops/loop_state.rs @@ -14,7 +14,7 @@ use super::*; /// through the loop when the loop contains a `switch` that could theoretically /// skip iterations. The variable `base` is assigned 2 outside the loop and /// never modified inside the loop body (which contains a switch on the loop -/// index). After constant propagation, `base ^ 3` must be folded to `8.0`. +/// index). After constant propagation, `base ^ 3` must be folded to `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_switch() { let program = vec![ @@ -54,7 +54,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_switch( assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -62,7 +62,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_switch( /// through the loop when the loop body contains a `try` statement. The variable /// `base` is assigned 2 outside the loop and never modified inside the loop body /// (which contains a try/catch on the loop index). After constant propagation, -/// `base ^ 3` must be folded to `8.0`. +/// `base ^ 3` must be folded to `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_try() { let program = vec![ @@ -100,7 +100,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_try() { assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -108,7 +108,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_with_try() { /// through the loops when the inner loop contains local statements. The variable /// `base` is assigned 2 before the outer for loop and never modified inside either /// loop (which modifies loop indices `i` and `j`). After constant propagation, -/// `base ^ 3` must be folded to `8.0`. +/// `base ^ 3` must be folded to `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_nested_loops() { let program = vec![ @@ -158,7 +158,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_nested_loops() { assert_eq!( propagated[3], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -167,7 +167,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_nested_loops() { /// (`items`). The variable `base` is assigned 2 outside the loop and never modified. /// The loop body performs `ArrayPush` and `ArrayAssign` on `items`, which must not /// be treated as modifications of `base`. After constant propagation, `base ^ 3` -/// must be folded to `8.0`. +/// must be folded to `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_loop_local_array_writes() { let program = vec![ @@ -210,7 +210,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_local_array_ assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -219,7 +219,7 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_local_array_ /// (`box`). The variable `base` is assigned 2 outside the loop and never modified. /// The loop body performs `PropertyAssign`, `PropertyArrayPush`, and /// `PropertyArrayAssign` on `$box->...`, which must not be treated as modifications -/// of `base`. After constant propagation, `base ^ 3` must be folded to `8.0`. +/// of `base`. After constant propagation, `base ^ 3` must be folded to `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_across_loop_property_writes() { let program = vec![ @@ -272,6 +272,6 @@ fn test_propagate_constants_preserves_unmodified_scalar_across_loop_property_wri assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/propagate/loops/while_loops.rs b/src/optimize/tests/propagate/loops/while_loops.rs index 714edd94dc..bc492a7a0f 100644 --- a/src/optimize/tests/propagate/loops/while_loops.rs +++ b/src/optimize/tests/propagate/loops/while_loops.rs @@ -31,7 +31,7 @@ fn test_propagate_constants_preserves_scalar_across_while_false_body_writes() { assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -55,7 +55,7 @@ fn test_propagate_constants_tracks_assignment_through_do_while_false() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -82,7 +82,7 @@ fn test_propagate_constants_tracks_assignment_through_while_true_break() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -120,7 +120,7 @@ fn test_propagate_constants_merges_branch_breaks_through_while_true() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -147,13 +147,13 @@ fn test_propagate_constants_tracks_continue_through_do_while_false() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that constant propagation preserves unmodified scalar values inside a while /// loop body. The variable `base` is assigned before the loop and never modified -/// inside the loop, so the echo statement should be folded to a literal `8.0`. +/// inside the loop, so the echo statement should be folded to a literal `8`. #[test] fn test_propagate_constants_preserves_unmodified_scalar_inside_while_loop_body() { let program = vec![ @@ -185,6 +185,6 @@ fn test_propagate_constants_preserves_unmodified_scalar_inside_while_loop_body() assert_eq!( body[0], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/propagate/straight_line.rs b/src/optimize/tests/propagate/straight_line.rs index fd120a9364..493d6f23ad 100644 --- a/src/optimize/tests/propagate/straight_line.rs +++ b/src/optimize/tests/propagate/straight_line.rs @@ -12,7 +12,7 @@ use super::*; /// Tests that integer literals assigned to sequential local variables are propagated /// through straight-line code (no control flow). The expression `x ** y` is folded to -/// `8.0` because both `x = 2` and `y = 3` are known constant values at the echo site. +/// `8` because both `x = 2` and `y = 3` are known constant values at the echo site. #[test] fn test_propagate_constants_through_straight_line_locals() { let program = vec![ @@ -28,14 +28,14 @@ fn test_propagate_constants_through_straight_line_locals() { vec![ Stmt::assign("x", Expr::int_lit(2)), Stmt::assign("y", Expr::int_lit(3)), - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())), + Stmt::echo(Expr::int_lit(8)), ] ); } /// Tests that when both branches of an If statement assign the same constant value /// to a variable, the variable is treated as a uniform constant after the If. -/// The second statement (`echo base ** 3`) should fold to `8.0` because `base` is +/// The second statement (`echo base ** 3`) should fold to `8` because `base` is /// known to be `2` regardless of which branch executes. #[test] fn test_propagate_constants_merges_identical_if_assignments() { @@ -56,7 +56,7 @@ fn test_propagate_constants_merges_identical_if_assignments() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } @@ -208,7 +208,7 @@ fn test_propagate_constants_invalidates_by_ref_variadic_function_args() { /// Tests that when both branches of a ternary expression are the same constant, /// the resulting assignment is treated as a uniform constant. `base = flag ? 2 : 2` -/// always yields `2`, so `base ** 3` folds to `8.0`. +/// always yields `2`, so `base ** 3` folds to `8`. #[test] fn test_propagate_constants_tracks_uniform_ternary_assignment() { let program = vec![ @@ -230,14 +230,14 @@ fn test_propagate_constants_tracks_uniform_ternary_assignment() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that when all arms of a match expression and its default clause yield the /// same constant value, the resulting assignment is treated as a uniform constant. /// `base = match(flag) { 1 => 2, default => 2 }` always yields `2`, so `base ** 3` -/// folds to `8.0`. +/// folds to `8`. #[test] fn test_propagate_constants_tracks_uniform_match_assignment() { let program = vec![ @@ -259,14 +259,14 @@ fn test_propagate_constants_tracks_uniform_match_assignment() { assert_eq!( propagated[1], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } /// Tests that when a match expression's subject is a known constant, the optimizer /// can determine which arm fires and propagate the resulting constant. Here the /// subject `mode = 1` means the first arm matches, so `base = 2` and `base ** 3` -/// folds to `8.0`. +/// folds to `8`. #[test] fn test_propagate_constants_tracks_known_match_assignment() { let program = vec![ @@ -290,7 +290,7 @@ fn test_propagate_constants_tracks_known_match_assignment() { assert_eq!(propagated[1], Stmt::assign("base", Expr::int_lit(2))); assert_eq!( propagated[2], - Stmt::echo(Expr::new(ExprKind::FloatLiteral(8.0), Span::dummy())) + Stmt::echo(Expr::int_lit(8)) ); } diff --git a/src/optimize/tests/prune.rs b/src/optimize/tests/prune.rs index 8b1134b106..ea8b7f4642 100644 --- a/src/optimize/tests/prune.rs +++ b/src/optimize/tests/prune.rs @@ -10,6 +10,8 @@ use super::*; +mod switch_loose_comparison; + /// Verifies that constant-false conditions in if/elseif chains are pruned, /// keeping only the first truthy branch (or the else body if all conditions /// are false). The else branch is selected when the second elseif condition diff --git a/src/optimize/tests/prune/switch_loose_comparison.rs b/src/optimize/tests/prune/switch_loose_comparison.rs new file mode 100644 index 0000000000..1b8064bc0b --- /dev/null +++ b/src/optimize/tests/prune/switch_loose_comparison.rs @@ -0,0 +1,97 @@ +//! Purpose: +//! Regression tests pinning constant `switch` selection to PHP's loose (`==`) case comparison. +//! +//! Called from: +//! - `crate::optimize::tests` through Rust's test harness. +//! +//! Key details: +//! - Every expected branch was confirmed by running the equivalent `switch` under `php -r` on +//! PHP 8.4.20; the fixtures use `echo ` bodies so the selected branch is unambiguous. +//! - PHP 8 changed the number-versus-non-numeric-string rule, so `switch (0) { case "foo": }` +//! must *not* match — the pass has to keep that. + +use super::*; + +/// Builds `switch (subject) { case pattern: echo 1; break; default: echo 2; }` and returns the +/// statements the pruner produced. +fn prune_switch(subject: Expr, pattern: Expr) -> Vec { + prune_constant_control_flow(vec![Stmt::new( + StmtKind::Switch { + subject, + cases: vec![( + vec![pattern], + vec![ + Stmt::echo(Expr::int_lit(1)), + Stmt::new(StmtKind::Break(1), Span::dummy()), + ], + )], + default: Some(vec![Stmt::echo(Expr::int_lit(2))]), + }, + Span::dummy(), + )]) +} + +/// Builds the literal named by the switch fixture table. +fn switch_operand(name: &str) -> Expr { + match name { + "null" => Expr::new(ExprKind::Null, Span::dummy()), + "false" => Expr::new(ExprKind::BoolLiteral(false), Span::dummy()), + "true" => Expr::new(ExprKind::BoolLiteral(true), Span::dummy()), + "i0" => Expr::int_lit(0), + "i1" => Expr::int_lit(1), + "i2" => Expr::int_lit(2), + "im1" => Expr::int_lit(-1), + "f0" => Expr::float_lit(0.0), + "f1_5" => Expr::float_lit(1.5), + "f2" => Expr::float_lit(2.0), + "s_empty" => Expr::string_lit(""), + "s_1" => Expr::string_lit("1"), + "s_1_5" => Expr::string_lit("1.5"), + "s_a" => Expr::string_lit("a"), + "s_abc" => Expr::string_lit("abc"), + "s_ABC" => Expr::string_lit("ABC"), + "s_foo" => Expr::string_lit("foo"), + other => panic!("unknown switch operand {other}"), + } +} + +/// PHP 8.4 `switch (subject) { case pattern: ... }` outcomes: `true` means the case is selected. +#[rustfmt::skip] +const PHP_SWITCH_CASES: &[(&str, &str, bool)] = &[ + // B6: `switch (2) { case true: }` matched `true` against the integer `1`; PHP compares + // with `==`, and `2 == true` is `(bool) 2`. + ("i2", "true", true), + ("im1", "true", true), + ("s_a", "true", true), + ("i0", "false", true), + ("f0", "false", true), + ("i0", "null", true), + ("null", "false", true), + ("null", "i0", true), + ("null", "s_empty", true), + ("s_empty", "null", true), + ("s_1", "i1", true), + ("f1_5", "s_1_5", true), + ("f2", "i2", true), + // PHP 8 string/number rules. + ("i1", "s_abc", false), + ("s_foo", "i0", false), + ("i0", "s_foo", false), + ("s_abc", "s_ABC", false), + ("i0", "true", false), + ("i2", "false", false), +]; + +/// Verifies constant `switch` selection matches PHP's `==` case comparison. +#[test] +fn test_prune_switch_case_uses_php_loose_equality() { + for &(subject, pattern, selected) in PHP_SWITCH_CASES { + let pruned = prune_switch(switch_operand(subject), switch_operand(pattern)); + let expected = if selected { 1 } else { 2 }; + assert_eq!( + pruned, + vec![Stmt::echo(Expr::int_lit(expected))], + "switch ({subject}) {{ case {pattern}: }}" + ); + } +} diff --git a/src/parser/alt_syntax.rs b/src/parser/alt_syntax.rs new file mode 100644 index 0000000000..ab674b892e --- /dev/null +++ b/src/parser/alt_syntax.rs @@ -0,0 +1,135 @@ +//! Purpose: +//! Parses PHP's alternative control-structure syntax (`:` … `endif;`, `endwhile;`, +//! `endfor;`, `endforeach;`, `endswitch;`) and shares the brace-vs-colon body decision +//! with every control statement parser. +//! +//! Called from: +//! - `crate::parser::control` for `if`/`while`/`for`/`foreach`/`switch` bodies. +//! +//! Key details: +//! - Alternative bodies desugar into exactly the same `StmtKind` bodies as the brace forms, +//! so no later pass needs to distinguish them. +//! - Statement errors inside a segment are collected (like `parse_block`) so one broken +//! statement does not hide the rest of the block. + +use crate::errors::CompileError; +use crate::lexer::{SpannedToken, Token}; +use crate::parser::ast::Stmt; +use crate::parser::stmt::{ + expect_semicolon, expect_token, parse_body, parse_stmt, recover_to_statement_boundary, +}; + +/// Tokens that end one segment of an alternative-syntax `if` (`then` and `elseif` bodies). +pub(crate) const IF_SEGMENT_STOPS: &[Token] = &[Token::ElseIf, Token::Else, Token::EndIf]; + +/// Returns true when the body starting at `pos` uses PHP's alternative `:` … `endX;` syntax. +pub(crate) fn starts_alternative_body(tokens: &[SpannedToken], pos: usize) -> bool { + matches!(tokens.get(pos).map(|(token, _)| token), Some(Token::Colon)) +} + +/// Parses statements until one of `stop` (or EOF) is reached, leaving the stop token unconsumed. +/// +/// Nested statement errors are collected and returned together, mirroring `parse_block`, so a +/// single malformed statement inside an alternative block still reports the following ones. +pub(crate) fn parse_alternative_stmts( + tokens: &[SpannedToken], + pos: &mut usize, + stop: &[Token], +) -> Result, CompileError> { + let mut body = Vec::new(); + let mut errors = Vec::new(); + + while *pos < tokens.len() + && tokens[*pos].0 != Token::Eof + && !stop.contains(&tokens[*pos].0) + { + match parse_stmt(tokens, pos) { + Ok(stmt) => body.push(stmt), + Err(error) => { + errors.extend(error.flatten()); + recover_to_statement_boundary(tokens, pos); + } + } + } + + if errors.is_empty() { + Ok(body) + } else { + Err(CompileError::from_many(errors)) + } +} + +/// Parses a control-structure body in either brace/single-statement form or PHP's alternative +/// `:` … `endX;` form, consuming the terminator keyword and its trailing `;` in the latter case. +/// +/// `terminator` is the closing keyword token for this statement (e.g. `Token::EndWhile`) and +/// `keyword` its spelling, used only for the diagnostic when the block is left unterminated. +pub(crate) fn parse_control_body( + tokens: &[SpannedToken], + pos: &mut usize, + terminator: &Token, + keyword: &str, +) -> Result, CompileError> { + if !starts_alternative_body(tokens, *pos) { + return parse_body(tokens, pos); + } + + *pos += 1; + let body = parse_alternative_stmts(tokens, pos, std::slice::from_ref(terminator))?; + close_alternative_block(tokens, pos, terminator, keyword)?; + Ok(body) +} + +/// Rejects an alternative-syntax (`:`) branch body opened inside a brace-form control structure. +/// +/// PHP requires one `if` chain to use a single style throughout, so `if (…) { … } else: … endif;` +/// is a syntax error. Reporting it here names the mixing instead of leaving a bare "Unexpected +/// token: Colon" at the branch body. +pub(crate) fn reject_mixed_branch_body( + tokens: &[SpannedToken], + pos: usize, + keyword: &str, +) -> Result<(), CompileError> { + if !starts_alternative_body(tokens, pos) { + return Ok(()); + } + Err(CompileError::new( + tokens[pos].1.span, + &format!( + "Cannot mix brace and alternative syntax in one if statement: '{}' opens a ':' body \ + but the 'if' used braces. Use either braces throughout or ':' … 'endif;' throughout", + keyword + ), + )) +} + +/// Returns the diagnostic for an `endif`/`endwhile`/`endfor`/`endforeach`/`endswitch` keyword +/// that appears where no alternative-syntax block is open. +/// +/// `keyword` is the terminator's spelling, taken from the token itself so the message repeats +/// exactly what the source wrote. +pub(crate) fn unopened_terminator_error(keyword: &str, span: crate::span::Span) -> CompileError { + CompileError::new( + span, + &format!( + "Unexpected '{}': there is no open alternative-syntax block for it to close", + keyword + ), + ) +} + +/// Consumes the `endX` terminator keyword and its mandatory `;`. +pub(crate) fn close_alternative_block( + tokens: &[SpannedToken], + pos: &mut usize, + terminator: &Token, + keyword: &str, +) -> Result<(), CompileError> { + expect_token( + tokens, + pos, + terminator, + &format!("Expected '{}' to close the alternative-syntax block", keyword), + )?; + expect_semicolon(tokens, pos) +} diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index 625a7d8883..97d28dbf6a 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -25,6 +25,10 @@ pub struct Stmt { pub span: Span, /// Physical source profile retained after includes and autoloaded statements are merged. pub source_mode: crate::source::SourceMode, + /// Whether the physical file this statement was parsed from opened with + /// `declare(strict_types=1)`. Retained alongside `source_mode` because the directive is + /// per-file and the type checker only ever sees the merged program. + pub strict_types: bool, /// PHP attributes attached to this statement. Only populated for /// declaration kinds (`ClassDecl`, `FunctionDecl`, etc.); the parser /// rejects attributes on non-declaration statements. @@ -38,6 +42,7 @@ impl Stmt { kind, span, source_mode: crate::source::current_parse_mode(), + strict_types: crate::source::current_strict_types(), attributes: Vec::new(), } } @@ -52,9 +57,22 @@ impl Stmt { kind, span, source_mode: crate::source::current_parse_mode(), + strict_types: crate::source::current_strict_types(), attributes, } } + + /// Returns the physical-file profile this statement was parsed under. + /// + /// Statement-rewriting passes install it with `crate::source::scoped_parse_mode` before + /// rebuilding a statement, so the replacement inherits the original file's language mode + /// *and* its `strict_types` state instead of the compiler-internal defaults. + pub fn profile(&self) -> crate::source::SourceProfile { + crate::source::SourceProfile { + mode: self.source_mode, + strict_types: self.strict_types, + } + } } #[derive(Debug, Clone, PartialEq)] diff --git a/src/parser/control.rs b/src/parser/control.rs index 567f82f654..fabf557a63 100644 --- a/src/parser/control.rs +++ b/src/parser/control.rs @@ -7,15 +7,27 @@ //! //! Key details: //! - Control parsers must preserve PHP statement nesting and spans for later flow and diagnostic passes. +//! - Brace and alternative (`:` … `endX;`) bodies produce identical `StmtKind` shapes, so the +//! distinction never escapes this module. use crate::errors::CompileError; use crate::lexer::{SpannedToken, Token}; +use crate::parser::alt_syntax::{ + close_alternative_block, parse_alternative_stmts, parse_control_body, + reject_mixed_branch_body, starts_alternative_body, IF_SEGMENT_STOPS, +}; use crate::parser::ast::{BinOp, CatchClause, Expr, ExprKind, Stmt, StmtKind}; use crate::parser::expr::{parse_assignment_value_expr, parse_expr}; -use crate::parser::stmt::{expect_semicolon, expect_token, name_starts_at, parse_block, parse_body, parse_name}; +use crate::parser::stmt::{ + expect_semicolon, expect_token, name_starts_at, parse_block, parse_body, + parse_destructuring_pattern_unpack, parse_name, starts_destructuring_pattern, +}; use crate::span::Span; /// Parse: if (expr) { stmts } (elseif (expr) { stmts })* (else { stmts })? +/// +/// Also accepts PHP's alternative form `if (expr): … elseif (expr): … else: … endif;`, +/// which is delegated to `parse_alternative_if` and yields the same `StmtKind::If`. pub fn parse_if( tokens: &[SpannedToken], pos: &mut usize, @@ -26,6 +38,11 @@ pub fn parse_if( expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'if'")?; let condition = parse_expr(tokens, pos)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after if condition")?; + + if starts_alternative_body(tokens, *pos) { + return parse_alternative_if(tokens, pos, span, condition); + } + let then_body = parse_body(tokens, pos)?; let mut elseif_clauses = Vec::new(); @@ -40,10 +57,12 @@ pub fn parse_if( expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'elseif'")?; let cond = parse_expr(tokens, pos)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after elseif condition")?; + reject_mixed_branch_body(tokens, *pos, "elseif")?; let body = parse_body(tokens, pos)?; elseif_clauses.push((cond, body)); } else if tokens[*pos].0 == Token::Else { *pos += 1; + reject_mixed_branch_body(tokens, *pos, "else")?; else_body = Some(parse_body(tokens, pos)?); break; } else { @@ -62,6 +81,67 @@ pub fn parse_if( )) } +/// Parse the alternative `if` form: `: stmts (elseif (expr): stmts)* (else: stmts)? endif;`. +/// +/// `pos` points at the `:` that opened the `then` segment and `condition` is the already-parsed +/// `if` condition. PHP requires every branch of an alternative `if` to use the colon form and the +/// whole chain to be closed by `endif;`, so a brace body or a bare `else if` is rejected here. +fn parse_alternative_if( + tokens: &[SpannedToken], + pos: &mut usize, + span: Span, + condition: Expr, +) -> Result { + *pos += 1; + let then_body = parse_alternative_stmts(tokens, pos, IF_SEGMENT_STOPS)?; + + let mut elseif_clauses = Vec::new(); + let mut else_body = None; + + loop { + match tokens.get(*pos).map(|(token, _)| token) { + Some(Token::ElseIf) => { + *pos += 1; + expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'elseif'")?; + let cond = parse_expr(tokens, pos)?; + expect_token(tokens, pos, &Token::RParen, "Expected ')' after elseif condition")?; + expect_token( + tokens, + pos, + &Token::Colon, + "Expected ':' after elseif condition in an alternative-syntax if block", + )?; + let body = parse_alternative_stmts(tokens, pos, IF_SEGMENT_STOPS)?; + elseif_clauses.push((cond, body)); + } + Some(Token::Else) => { + *pos += 1; + expect_token( + tokens, + pos, + &Token::Colon, + "Expected ':' after 'else' in an alternative-syntax if block", + )?; + else_body = Some(parse_alternative_stmts(tokens, pos, &[Token::EndIf])?); + break; + } + _ => break, + } + } + + close_alternative_block(tokens, pos, &Token::EndIf, "endif")?; + + Ok(Stmt::new( + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + }, + span, + )) +} + /// Parse: ifdef SYMBOL { stmts } (else { stmts })? pub fn parse_ifdef( tokens: &[SpannedToken], @@ -94,7 +174,7 @@ pub fn parse_ifdef( )) } -/// Parse: while (expr) { stmts } +/// Parse: while (expr) { stmts }, or the alternative form `while (expr): stmts endwhile;`. pub fn parse_while( tokens: &[SpannedToken], pos: &mut usize, @@ -104,7 +184,7 @@ pub fn parse_while( expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'while'")?; let condition = parse_expr(tokens, pos)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after while condition")?; - let body = parse_body(tokens, pos)?; + let body = parse_control_body(tokens, pos, &Token::EndWhile, "endwhile")?; Ok(Stmt::new(StmtKind::While { condition, body }, span)) } @@ -130,6 +210,31 @@ pub fn parse_foreach( false }; + // `foreach ($pairs as [$a, $b])`: the value target is a destructuring pattern, so the + // loop binds a hidden temporary and the body starts by unpacking it. + if starts_destructuring_pattern(tokens, *pos) { + if first_by_ref { + return Err(CompileError::new( + span, + "Cannot take a reference to a destructuring pattern in foreach", + )); + } + let (value_var, unpack) = parse_foreach_pattern_target(tokens, pos, span)?; + expect_token(tokens, pos, &Token::RParen, "Expected ')' after foreach")?; + let loop_body = parse_control_body(tokens, pos, &Token::EndForeach, "endforeach")?; + let body = prepend_stmt(unpack, loop_body); + return Ok(Stmt::new( + StmtKind::Foreach { + array, + key_var: None, + value_var, + value_by_ref: false, + body, + }, + span, + )); + } + let first_var = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => n.clone(), _ => return Err(CompileError::new(span, "Expected variable after 'as'")), @@ -137,7 +242,7 @@ pub fn parse_foreach( *pos += 1; // Check for => (foreach $arr as $key => $value) - let (key_var, value_var, value_by_ref) = + let (key_var, value_var, value_by_ref, unpack) = if *pos < tokens.len() && tokens[*pos].0 == Token::DoubleArrow { if first_by_ref { return Err(CompileError::new( @@ -155,18 +260,34 @@ pub fn parse_foreach( } else { false }; - let val_var = match tokens.get(*pos).map(|(t, _)| t) { - Some(Token::Variable(n)) => n.clone(), - _ => return Err(CompileError::new(span, "Expected variable after '=>'")), - }; - *pos += 1; - (Some(first_var), val_var, value_by_ref) + // `foreach ($m as $k => [$a, $b])` destructures the value the same way. + if starts_destructuring_pattern(tokens, *pos) { + if value_by_ref { + return Err(CompileError::new( + span, + "Cannot take a reference to a destructuring pattern in foreach", + )); + } + let (val_var, unpack) = parse_foreach_pattern_target(tokens, pos, span)?; + (Some(first_var), val_var, false, Some(unpack)) + } else { + let val_var = match tokens.get(*pos).map(|(t, _)| t) { + Some(Token::Variable(n)) => n.clone(), + _ => return Err(CompileError::new(span, "Expected variable after '=>'")), + }; + *pos += 1; + (Some(first_var), val_var, value_by_ref, None) + } } else { - (None, first_var, first_by_ref) + (None, first_var, first_by_ref, None) }; expect_token(tokens, pos, &Token::RParen, "Expected ')' after foreach")?; - let body = parse_body(tokens, pos)?; + let body = parse_control_body(tokens, pos, &Token::EndForeach, "endforeach")?; + let body = match unpack { + Some(unpack) => prepend_stmt(unpack, body), + None => body, + }; Ok(Stmt::new( StmtKind::Foreach { @@ -180,6 +301,38 @@ pub fn parse_foreach( )) } +/// Parses a `foreach` value destructuring pattern into a hidden loop variable plus the +/// statement that unpacks it. +/// +/// The loop still binds one value per iteration, so the pattern becomes +/// `foreach (… as $tmp) { [pattern] = $tmp; … }`. The temporary is named from the pattern's +/// source position so nested loops in one function never collide. +fn parse_foreach_pattern_target( + tokens: &[SpannedToken], + pos: &mut usize, + span: Span, +) -> Result<(String, Stmt), CompileError> { + let pattern_span = tokens + .get(*pos) + .map(|(_, metadata)| metadata.span) + .unwrap_or(span); + let value_var = format!( + "__elephc_foreach_{}_{}", + pattern_span.line, pattern_span.col + ); + let source = Expr::new(ExprKind::Variable(value_var.clone()), pattern_span); + let unpack = parse_destructuring_pattern_unpack(tokens, pos, pattern_span, source)?; + Ok((value_var, unpack)) +} + +/// Returns `body` with `first` inserted as its first statement. +fn prepend_stmt(first: Stmt, body: Vec) -> Vec { + let mut stmts = Vec::with_capacity(body.len() + 1); + stmts.push(first); + stmts.extend(body); + stmts +} + /// Parse: do { stmts } while (expr); pub fn parse_do_while( tokens: &[SpannedToken], @@ -196,7 +349,7 @@ pub fn parse_do_while( Ok(Stmt::new(StmtKind::DoWhile { body, condition }, span)) } -/// Parse: for (init; condition; update) { stmts } +/// Parse: for (init; condition; update) { stmts }, or `for (…): stmts endfor;`. pub fn parse_for( tokens: &[SpannedToken], pos: &mut usize, @@ -230,7 +383,7 @@ pub fn parse_for( }; expect_token(tokens, pos, &Token::RParen, "Expected ')' after for clauses")?; - let body = parse_body(tokens, pos)?; + let body = parse_control_body(tokens, pos, &Token::EndFor, "endfor")?; Ok(Stmt::new( StmtKind::For { @@ -435,6 +588,9 @@ pub fn parse_assign_inline( } /// Parse: switch (expr) { case expr: stmts... case expr: stmts... default: stmts... } +/// +/// Also accepts PHP's alternative form `switch (expr): case …: … endswitch;`. Both forms +/// produce the same `StmtKind::Switch`; only the case-list terminator differs. pub fn parse_switch( tokens: &[SpannedToken], pos: &mut usize, @@ -444,37 +600,51 @@ pub fn parse_switch( expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'switch'")?; let subject = parse_expr(tokens, pos)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after switch expression")?; - expect_token(tokens, pos, &Token::LBrace, "Expected '{' after switch")?; + + let alternative = starts_alternative_body(tokens, *pos); + if alternative { + *pos += 1; + } else { + expect_token(tokens, pos, &Token::LBrace, "Expected '{' after switch")?; + } + // The case list ends at `}` in the brace form and at `endswitch` in the alternative form. + let close = if alternative { + Token::EndSwitch + } else { + Token::RBrace + }; let mut cases: Vec<(Vec, Vec)> = Vec::new(); let mut default: Option> = None; - while *pos < tokens.len() && tokens[*pos].0 != Token::RBrace { + while *pos < tokens.len() && tokens[*pos].0 != close && tokens[*pos].0 != Token::Eof { if tokens[*pos].0 == Token::Case { // Parse one or more case values let mut values = Vec::new(); while *pos < tokens.len() && tokens[*pos].0 == Token::Case { *pos += 1; values.push(parse_expr(tokens, pos)?); - expect_token(tokens, pos, &Token::Colon, "Expected ':' after case value")?; + expect_case_separator(tokens, pos, "Expected ':' after case value")?; } - // Parse case body (statements until next case/default/}) + // Parse case body (statements until the next case/default or the case-list end) let mut body = Vec::new(); while *pos < tokens.len() && tokens[*pos].0 != Token::Case && tokens[*pos].0 != Token::Default - && tokens[*pos].0 != Token::RBrace + && tokens[*pos].0 != close + && tokens[*pos].0 != Token::Eof { body.push(crate::parser::stmt::parse_stmt(tokens, pos)?); } cases.push((values, body)); } else if tokens[*pos].0 == Token::Default { *pos += 1; - expect_token(tokens, pos, &Token::Colon, "Expected ':' after 'default'")?; + expect_case_separator(tokens, pos, "Expected ':' after 'default'")?; let mut body = Vec::new(); while *pos < tokens.len() && tokens[*pos].0 != Token::Case - && tokens[*pos].0 != Token::RBrace + && tokens[*pos].0 != close + && tokens[*pos].0 != Token::Eof { body.push(crate::parser::stmt::parse_stmt(tokens, pos)?); } @@ -487,7 +657,11 @@ pub fn parse_switch( } } - expect_token(tokens, pos, &Token::RBrace, "Expected '}' to close switch")?; + if alternative { + close_alternative_block(tokens, pos, &Token::EndSwitch, "endswitch")?; + } else { + expect_token(tokens, pos, &Token::RBrace, "Expected '}' to close switch")?; + } Ok(Stmt::new( StmtKind::Switch { @@ -498,3 +672,21 @@ pub fn parse_switch( span, )) } + +/// Consumes the separator that terminates a `case`/`default` label. +/// +/// PHP accepts either `:` or `;` there, so both are allowed with the same meaning. +fn expect_case_separator( + tokens: &[SpannedToken], + pos: &mut usize, + message: &str, +) -> Result<(), CompileError> { + if matches!( + tokens.get(*pos).map(|(token, _)| token), + Some(Token::Semicolon) + ) { + *pos += 1; + return Ok(()); + } + expect_token(tokens, pos, &Token::Colon, message) +} diff --git a/src/parser/expr/pratt.rs b/src/parser/expr/pratt.rs index 44ab491fe8..02ccae069f 100644 --- a/src/parser/expr/pratt.rs +++ b/src/parser/expr/pratt.rs @@ -724,7 +724,7 @@ fn parse_instanceof_target( /// /// Precedence order (lowest to highest): `or` (1) < `xor` (3) < `and` (5) /// < `??` (9) < `||` (11) < `&&` (13) < `|` (15) < `^` (17) < `&` (19) -/// < `==`/`!=`/`===`/`!==` (21) < `<`/`>`/`<=`/`>=`/`<=>` (23) +/// < `==`/`!=`/`<>`/`===`/`!==` (21) < `<`/`>`/`<=`/`>=`/`<=>` (23) /// < `<<`/`>>` (25) < `.` (27) < `+`/`-` (29) < `*`/`/`/`%` (31) /// < `**` (37 right-assoc) fn infix_bp(token: &Token) -> Option<(BinOp, u8, u8)> { @@ -740,6 +740,7 @@ fn infix_bp(token: &Token) -> Option<(BinOp, u8, u8)> { Token::Ampersand => Some((BinOp::BitAnd, 19, 20)), Token::EqualEqual => Some((BinOp::Eq, 21, 22)), Token::NotEqual => Some((BinOp::NotEq, 21, 22)), + Token::LessGreater => Some((BinOp::NotEq, 21, 22)), Token::EqualEqualEqual => Some((BinOp::StrictEq, 21, 22)), Token::NotEqualEqual => Some((BinOp::StrictNotEq, 21, 22)), Token::Less => Some((BinOp::Lt, 23, 24)), diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index cd8499ebca..188ea1070a 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -517,6 +517,7 @@ fn parse_array_literal_with_terminator( first = false; continue; } + reject_reference_array_element(tokens, pos, closing)?; let expr = parse_expr(tokens, pos)?; if *pos < tokens.len() && tokens[*pos].0 == Token::DoubleArrow { if !is_assoc { @@ -524,6 +525,7 @@ fn parse_array_literal_with_terminator( } is_assoc = true; *pos += 1; + reject_reference_array_element(tokens, pos, closing)?; let value = parse_expr(tokens, pos)?; update_next_auto_key_from_explicit_key( &expr, @@ -557,6 +559,58 @@ fn parse_array_literal_with_terminator( } } +/// Rejects a by-reference array-literal element (`[&$x]`, `[$k => &$x]`, `array(&$x)`) with a +/// diagnostic that names the construct instead of a bare "Unexpected token: Ampersand". +/// +/// PHP stores such an element as a reference cell that aliases the source variable's storage, +/// so `$r = [&$a]; $r[0] = 9;` writes through to `$a`. elephc arrays hold plain values and its +/// only reference form points *into* array storage (`$b =& $a[0]`), never out of it, so the +/// construct cannot be honoured without either silently copying or leaving the array holding a +/// pointer to a stack slot it can outlive. Returns `Ok(())` when the element is not a reference. +/// +/// On rejection `pos` is advanced past the rest of the literal so statement recovery resumes +/// after it and does not report cascading errors for the remaining elements. +fn reject_reference_array_element( + tokens: &[SpannedToken], + pos: &mut usize, + closing: &Token, +) -> Result<(), CompileError> { + let Some((Token::Ampersand, metadata)) = tokens.get(*pos) else { + return Ok(()); + }; + let span = metadata.span; + skip_to_array_literal_end(tokens, pos, closing); + Err(CompileError::new( + span, + "Reference elements in array literals (`[&$x]`) are not supported: an array element \ + cannot alias a variable's storage. Assign the value instead, or use `$b =& $a[0]` \ + to alias an existing array element", + )) +} + +/// Advances `pos` past the remainder of the current array literal, including its `closing` +/// token, tracking nested `[`/`(` pairs so inner literals and calls are skipped whole. +/// +/// Stops at end-of-input if the literal is unterminated, leaving `pos` at `Eof`. +fn skip_to_array_literal_end(tokens: &[SpannedToken], pos: &mut usize, closing: &Token) { + let mut depth = 0usize; + while let Some((token, _)) = tokens.get(*pos) { + match token { + Token::Eof => return, + Token::LBracket | Token::LParen => depth += 1, + Token::RBracket | Token::RParen => { + if depth == 0 && token == closing { + *pos += 1; + return; + } + depth = depth.saturating_sub(1); + } + _ => {} + } + *pos += 1; + } +} + /// Converts positional items parsed before a keyed array entry into integer-keyed pairs. fn promote_indexed_array_items_to_assoc( elems: &mut Vec, diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index fd6e52d820..66717b609d 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -631,6 +631,17 @@ pub(super) fn parse_named_expr( { return super::prefix::parse_legacy_array_literal(tokens, pos, span); } + // `buffer_new<>` lexes as the single `<>` token (PHP's `!=` alias). + if name.parts.len() == 1 + && name.parts[0] == "buffer_new" + && *pos < tokens.len() + && tokens[*pos].0 == Token::LessGreater + { + return Err(CompileError::new( + span, + "Expected buffer element type after 'buffer_new<'", + )); + } if name.parts.len() == 1 && name.parts[0] == "buffer_new" && *pos < tokens.len() @@ -662,6 +673,18 @@ pub(super) fn parse_named_expr( span, )); } + // `ptr_cast<>` lexes as the single `<>` token (PHP's `!=` alias), so the empty + // type list is recognized here to keep reporting the missing type name. + if name.parts.len() == 1 + && name.parts[0] == "ptr_cast" + && *pos < tokens.len() + && tokens[*pos].0 == Token::LessGreater + { + return Err(CompileError::new( + span, + "Expected type name after 'ptr_cast<'", + )); + } if name.parts.len() == 1 && name.parts[0] == "ptr_cast" && *pos < tokens.len() diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5e90ad37d5..6b8607c49e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,6 +8,8 @@ //! Key details: //! - Parser output preserves spans and PHP syntax shape for later passes to rewrite safely. +/// PHP alternative control-structure syntax (`:` … `endif;`) body parsing helpers. +mod alt_syntax; /// Defines AST node types representing the PHP syntax tree produced by the parser. pub mod ast; mod attributes; @@ -97,7 +99,9 @@ pub fn parse_with_recovery_in_mode( tokens: &[SpannedToken], mode: crate::source::SourceMode, ) -> Result> { - crate::source::with_parse_mode(mode, || parse_with_recovery_inner(tokens)) + crate::source::with_parse_mode(crate::source::SourceProfile::new(mode), || { + parse_with_recovery_inner(tokens) + }) } /// Implements recovery parsing after the source-mode scope has been installed. diff --git a/src/parser/stmt/assign.rs b/src/parser/stmt/assign.rs index 8a0bbeb5d2..86f6706b19 100644 --- a/src/parser/stmt/assign.rs +++ b/src/parser/stmt/assign.rs @@ -14,6 +14,10 @@ mod locals; mod postfix; mod simple; +pub(crate) use list::{ + parse_destructuring_pattern_unpack, + starts_destructuring_pattern, +}; pub(super) use list::{ parse_list_construct_unpack, parse_list_unpack, @@ -30,6 +34,7 @@ pub(crate) use postfix::{ }; pub(super) use postfix::{ try_parse_postfix_assignment, + try_parse_postfix_incdec, try_parse_scoped_postfix_incdec, try_parse_scoped_property_assignment, }; diff --git a/src/parser/stmt/assign/list.rs b/src/parser/stmt/assign/list.rs index f920d6a77d..497a045dfb 100644 --- a/src/parser/stmt/assign/list.rs +++ b/src/parser/stmt/assign/list.rs @@ -66,6 +66,48 @@ pub(in crate::parser::stmt) fn parse_list_construct_unpack( Ok(lower_list_unpack(pattern, value, span)) } +/// Parses a destructuring pattern used as a `foreach` value target and lowers it into the +/// statement that unpacks `source` into the pattern's targets. +/// +/// Accepts both spellings PHP allows there — `[$a, $b]` and `list($a, $b)` — and consumes +/// exactly the pattern, leaving `*pos` on the token that follows it (`)` for a `foreach` +/// value target). `source` is the expression the loop assigns each element to, normally a +/// hidden temporary the caller also names as the loop's value variable. +/// +/// The returned statement is the very same lowering `[$a, $b] = $source;` produces, so +/// nested, keyed, skipped, and property/array-element targets all behave identically inside +/// and outside `foreach`. +pub(crate) fn parse_destructuring_pattern_unpack( + tokens: &[SpannedToken], + pos: &mut usize, + span: Span, + source: Expr, +) -> Result { + let pattern = match tokens.get(*pos).map(|(token, _)| token) { + Some(Token::LBracket) => parse_bracket_list_pattern(tokens, pos, span)?, + Some(Token::Identifier(name)) if name.eq_ignore_ascii_case("list") => { + parse_list_construct_pattern(tokens, pos, span)? + } + _ => return Err(CompileError::new(span, "Expected destructuring pattern")), + }; + Ok(lower_list_unpack(pattern, source, span)) +} + +/// Returns true when the token at `pos` opens a destructuring pattern (`[` or `list(`). +pub(crate) fn starts_destructuring_pattern( + tokens: &[SpannedToken], + pos: usize, +) -> bool { + match tokens.get(pos).map(|(token, _)| token) { + Some(Token::LBracket) => true, + Some(Token::Identifier(name)) if name.eq_ignore_ascii_case("list") => matches!( + tokens.get(pos + 1).map(|(token, _)| token), + Some(Token::LParen) + ), + _ => false, + } +} + /// Represents a list destructuring pattern with ordered entries. #[derive(Debug, Clone)] struct ListPattern { diff --git a/src/parser/stmt/assign/locals.rs b/src/parser/stmt/assign/locals.rs index 0d92e6ad3d..cb3ceda0d0 100644 --- a/src/parser/stmt/assign/locals.rs +++ b/src/parser/stmt/assign/locals.rs @@ -63,6 +63,15 @@ pub(in crate::parser::stmt) fn parse_incdec_stmt( return Err(CompileError::new(span, "Invalid increment target")); } + // `++$this->n;`, `++$obj->n;`, and `++$a[0];` target storage the simple local path + // cannot name. Statement position discards the operator's value, so they lower + // through the same read-modify-write shape as their postfix spellings. + if starts_complex_incdec_target(tokens, *pos) { + let lhs_expr = crate::parser::expr::parse_expr(tokens, pos)?; + expect_semicolon(tokens, pos)?; + return super::postfix::lower_postfix_incdec_assignment(lhs_expr, is_increment, span); + } + let name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => n.clone(), _ => { @@ -85,6 +94,23 @@ pub(in crate::parser::stmt) fn parse_incdec_stmt( Ok(Stmt::new(StmtKind::ExprStmt(expr), span)) } +/// Returns true when the tokens after a prefix `++`/`--` name a property, array element, +/// or `$this` member rather than a plain local variable. +/// +/// `$this` always continues into a member access, and a variable is only a complex target +/// when it is followed by `->`, `?->`, or `[`. Everything else keeps the plain +/// `PreIncrement`/`PreDecrement` local path. +fn starts_complex_incdec_target(tokens: &[SpannedToken], pos: usize) -> bool { + match tokens.get(pos).map(|(token, _)| token) { + Some(Token::This) => true, + Some(Token::Variable(_)) => matches!( + tokens.get(pos + 1).map(|(token, _)| token), + Some(Token::Arrow) | Some(Token::QuestionArrow) | Some(Token::LBracket) + ), + _ => false, + } +} + /// Parses a `global $var, ...;` declaration statement. /// Consumes the `global` keyword, then collects a comma-separated list of variable names /// until a semicolon. Returns a `StmtKind::Global` node. diff --git a/src/parser/stmt/assign/postfix.rs b/src/parser/stmt/assign/postfix.rs index ef81a60bc9..a1ca7a81d8 100644 --- a/src/parser/stmt/assign/postfix.rs +++ b/src/parser/stmt/assign/postfix.rs @@ -595,7 +595,10 @@ fn lower_effectful_postfix_assignment( } /// Lowers discarded post-increment/decrement to the existing assignment statement forms. -fn lower_postfix_incdec_assignment( +/// +/// Statement position discards the operator's value, so prefix `++$obj->n;` lowers through +/// here too: with the result unused, `++X` and `X++` are both `X += 1`. +pub(in crate::parser::stmt::assign) fn lower_postfix_incdec_assignment( lhs_expr: Expr, is_increment: bool, span: Span, diff --git a/src/parser/stmt/declare.rs b/src/parser/stmt/declare.rs index 431f499686..c5edb84929 100644 --- a/src/parser/stmt/declare.rs +++ b/src/parser/stmt/declare.rs @@ -6,7 +6,12 @@ //! - `crate::parser::stmt::parse_stmt()` when the current token is `declare`. //! //! Key details: -//! - Directives are compile-time syntax only because elephc always uses strict typing. +//! - `strict_types` is recorded on the parser's per-file source profile +//! (`crate::source::declare_strict_types`), which stamps every statement parsed afterwards. +//! PHP requires the directive to be a file's first statement, so "afterwards" is exactly +//! "the rest of this file"; the type checker reads the stamp back per statement to pick +//! between PHP's strict and coercive parameter binding. +//! - Every other directive (`ticks`, `encoding`) is compile-time syntax only. //! - Bodies lower through `Synthetic` so they execute in the enclosing scope. use crate::errors::CompileError; @@ -28,7 +33,7 @@ pub(super) fn parse_declare( *pos += 1; expect_token(tokens, pos, &Token::LParen, "Expected '(' after 'declare'")?; - let has_strict_types = parse_directives(tokens, pos, span)?; + let strict_types = parse_directives(tokens, pos, span)?; expect_token( tokens, pos, @@ -36,7 +41,7 @@ pub(super) fn parse_declare( "Expected ')' after declare directives", )?; - if has_strict_types && declare_pos != 1 { + if strict_types.is_some() && declare_pos != 1 { return Err(CompileError::new( span, "strict_types declaration must be the very first statement in the script", @@ -48,10 +53,15 @@ pub(super) fn parse_declare( Some(Token::Semicolon) ) { *pos += 1; + // Applied only once the directive has passed every placement and form check, so a + // rejected `declare` never leaves the rest of the file typed under it. + if let Some(enabled) = strict_types { + crate::source::declare_strict_types(enabled); + } return Ok(Stmt::new(StmtKind::Synthetic(Vec::new()), span)); } - if has_strict_types { + if strict_types.is_some() { return Err(CompileError::new( span, "strict_types declaration must not use block mode", @@ -73,13 +83,17 @@ pub(super) fn parse_declare( Ok(Stmt::new(StmtKind::Synthetic(body), span)) } -/// Parses one or more directive/literal pairs and reports whether `strict_types` occurred. +/// Parses one or more directive/literal pairs. +/// +/// Returns `Some(true)` for `strict_types=1`, `Some(false)` for `strict_types=0`, and `None` +/// when the list holds no `strict_types` directive at all. The caller needs the three-way answer +/// because only a present directive is subject to PHP's placement and block-form restrictions. fn parse_directives( tokens: &[SpannedToken], pos: &mut usize, declare_span: Span, -) -> Result { - let mut has_strict_types = false; +) -> Result, CompileError> { + let mut strict_types = None; loop { let (name, name_span) = match tokens.get(*pos) { @@ -112,12 +126,15 @@ fn parse_directives( } if name.eq_ignore_ascii_case("strict_types") { - has_strict_types = true; - if !matches!(integer_value, Some(0 | 1)) { - return Err(CompileError::new( - name_span, - "strict_types declaration must have 0 or 1 as its value", - )); + match integer_value { + Some(0) => strict_types = Some(false), + Some(1) => strict_types = Some(true), + _ => { + return Err(CompileError::new( + name_span, + "strict_types declaration must have 0 or 1 as its value", + )); + } } } @@ -127,7 +144,7 @@ fn parse_directives( *pos += 1; } - Ok(has_strict_types) + Ok(strict_types) } /// Consumes a PHP declare literal and returns its integer value when it is an integer. diff --git a/src/parser/stmt/ffi.rs b/src/parser/stmt/ffi.rs index 653edf282b..afa497c6a8 100644 --- a/src/parser/stmt/ffi.rs +++ b/src/parser/stmt/ffi.rs @@ -36,6 +36,11 @@ fn parse_c_type(tokens: &[SpannedToken], pos: &mut usize) -> Result Ok(CType::Void), "callable" => Ok(CType::Callable), "ptr" => { + // `ptr<>` lexes as the single `<>` token (PHP's `!=` alias), so the + // empty type list is recognized here to keep the diagnostic accurate. + if *pos < tokens.len() && tokens[*pos].0 == Token::LessGreater { + return Err(CompileError::new(span, "Expected type name after 'ptr<'")); + } // Check for ptr if *pos < tokens.len() && tokens[*pos].0 == Token::Less { *pos += 1; // consume < diff --git a/src/parser/stmt/goto_unsupported.rs b/src/parser/stmt/goto_unsupported.rs new file mode 100644 index 0000000000..74ac08fbf7 --- /dev/null +++ b/src/parser/stmt/goto_unsupported.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Produces the explicit "not supported" diagnostics for PHP's `goto` statement and its +//! target labels, so both are named in the error instead of surfacing as generic +//! "Unexpected token" / "Expected ';'" syntax noise. +//! +//! Called from: +//! - `crate::parser::stmt::parse_stmt_dispatch()` for `goto` and for `label:` at statement position. +//! +//! Key details: +//! - `goto` is lexed as a reserved keyword (`Token::Goto`) so the diagnostic can name it and so +//! the word cannot be taken as a function name, matching PHP's reserved-word list. +//! - elephc lowers structured control flow to EIR through statement-shaped passes (termination +//! analysis, flow-sensitive type narrowing, loop/branch pruning, constant propagation). An +//! arbitrary intra-function jump would invalidate those structural assumptions, so the +//! construct is rejected outright rather than partially supported. +//! - A label is only reachable through `goto`, so both spellings report the same limitation. + +use crate::errors::CompileError; +use crate::lexer::{SpannedToken, Token}; +use crate::span::Span; + +/// Shared tail explaining the supported alternatives for both `goto` diagnostics. +const GOTO_ALTERNATIVES: &str = + "restructure the jump with `break`, `continue`, a loop flag, or an early `return`"; + +/// Returns the diagnostic for a `goto` statement, which elephc does not support. +pub(super) fn reject_goto_statement(span: Span) -> CompileError { + CompileError::new( + span, + &format!( + "`goto` is not supported: elephc compiles structured control flow only, so a jump \ + to an arbitrary label inside a function has no lowering. Please {}", + GOTO_ALTERNATIVES + ), + ) +} + +/// Returns true when the token at `pos` starts a PHP `goto` label (`name:`) at statement position. +/// +/// Only a plain identifier immediately followed by `:` qualifies. `Foo::bar()` lexes `::` as one +/// token, alternative-syntax `else:`/`case`/`default` use their own keyword tokens, and a ternary +/// reaches its `:` only after a `?`, so none of them are mistaken for a label. +pub(super) fn starts_goto_label(tokens: &[SpannedToken], pos: usize) -> bool { + matches!(tokens.get(pos).map(|(token, _)| token), Some(Token::Identifier(_))) + && matches!(tokens.get(pos + 1).map(|(token, _)| token), Some(Token::Colon)) +} + +/// Returns the diagnostic for a `goto` target label, naming the label that was declared. +pub(super) fn reject_goto_label(label: &str, span: Span) -> CompileError { + CompileError::new( + span, + &format!( + "`goto` labels are not supported: the label `{}:` can only be reached by `goto`, \ + which elephc does not support. Please {}", + label, GOTO_ALTERNATIVES + ), + ) +} diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 2209e5c261..4026d980b2 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -12,6 +12,7 @@ mod assign; mod blocks; mod declare; mod ffi; +mod goto_unsupported; mod names; mod namespace_use; mod oop; @@ -34,6 +35,7 @@ pub(crate) use params::{looks_like_typed_param, parse_type_expr}; pub(crate) use assign::can_replay_assignment_target; pub(crate) use blocks::{expect_semicolon, expect_token}; pub(crate) use names::{name_part_from_token, name_starts_at, parse_name, parse_unqualified_name}; +pub(crate) use assign::{parse_destructuring_pattern_unpack, starts_destructuring_pattern}; pub(crate) use recovery::recover_to_statement_boundary; /// Parses a single PHP statement, including optional PHP 8 attribute groups. @@ -144,6 +146,25 @@ fn parse_stmt_dispatch( Token::RequireOnce => simple::parse_include(tokens, pos, span, true, true), Token::Const => simple::parse_const_decl(tokens, pos, span), Token::Global => assign::parse_global(tokens, pos, span), + Token::EndIf + | Token::EndWhile + | Token::EndFor + | Token::EndForeach + | Token::EndSwitch + | Token::EndDeclare => { + let keyword = tokens[*pos] + .0 + .canonical_word_spelling() + .unwrap_or("end-block keyword"); + *pos += 1; + Err(crate::parser::alt_syntax::unopened_terminator_error( + keyword, span, + )) + } + Token::Goto => Err(goto_unsupported::reject_goto_statement(span)), + Token::Identifier(label) if goto_unsupported::starts_goto_label(tokens, *pos) => { + Err(goto_unsupported::reject_goto_label(label, span)) + } Token::Static => { if *pos + 1 < tokens.len() && tokens[*pos + 1].0 == Token::DoubleColon { if let Some(stmt) = diff --git a/src/parser/stmt/params.rs b/src/parser/stmt/params.rs index b31106ccae..e0ec627e12 100644 --- a/src/parser/stmt/params.rs +++ b/src/parser/stmt/params.rs @@ -259,6 +259,14 @@ fn parse_atomic_type_expr( } Some(Token::Identifier(name)) if matches!(name.as_str(), "ptr" | "pointer") => { *pos += 1; + // `ptr<>` lexes as the single `<>` token (PHP's `!=` alias), so the empty + // type list is recognized here instead of reading as a bare `ptr`. + if *pos < tokens.len() && tokens[*pos].0 == Token::LessGreater { + return Err(CompileError::new( + span, + "Expected pointer target type inside ptr<...>", + )); + } if *pos < tokens.len() && tokens[*pos].0 == Token::Less { *pos += 1; let target = parse_name( @@ -280,6 +288,13 @@ fn parse_atomic_type_expr( } Some(Token::Identifier(name)) if name == "buffer" => { *pos += 1; + // `buffer<>` lexes as the single `<>` token (PHP's `!=` alias). + if *pos < tokens.len() && tokens[*pos].0 == Token::LessGreater { + return Err(CompileError::new( + span, + "Expected buffer element type after 'buffer<'", + )); + } expect_token(tokens, pos, &Token::Less, "Expected '<' after buffer")?; let inner = parse_type_expr(tokens, pos, span)?; expect_token( diff --git a/src/parser/stmt/recovery.rs b/src/parser/stmt/recovery.rs index a5e155d7ad..feb4f9dac1 100644 --- a/src/parser/stmt/recovery.rs +++ b/src/parser/stmt/recovery.rs @@ -37,7 +37,14 @@ pub(crate) fn recover_to_statement_boundary(tokens: &[SpannedToken], pos: &mut u *pos += 1; break; } - Token::RBrace | Token::EndDeclare | Token::Eof + Token::RBrace + | Token::EndDeclare + | Token::EndIf + | Token::EndWhile + | Token::EndFor + | Token::EndForeach + | Token::EndSwitch + | Token::Eof if paren_depth == 0 && bracket_depth == 0 => { break; @@ -69,6 +76,7 @@ pub(crate) fn recover_to_statement_boundary(tokens: &[SpannedToken], pos: &mut u | Token::Const | Token::Global | Token::Static + | Token::Goto | Token::Identifier(_) | Token::Self_ | Token::Parent diff --git a/src/parser/stmt/simple.rs b/src/parser/stmt/simple.rs index 4f3dc0bbd6..283212e190 100644 --- a/src/parser/stmt/simple.rs +++ b/src/parser/stmt/simple.rs @@ -14,7 +14,7 @@ use crate::parser::ast::{Expr, ExprKind, Stmt, StmtKind}; use crate::parser::expr::{parse_assignment_value_expr, parse_expr}; use crate::span::Span; -use super::assign::try_parse_postfix_assignment; +use super::assign::{try_parse_postfix_assignment, try_parse_postfix_incdec}; use super::{expect_semicolon, expect_token}; /// Parses `include`/`require` (with optional `_once`) statements. @@ -235,6 +235,11 @@ pub(super) fn parse_this_stmt( if let Some(stmt) = try_parse_postfix_assignment(tokens, pos, span)? { return Ok(stmt); } + // `$this->n++` and `$this->arr[0]++` are read-modify-write statements, exactly like + // `$obj->n++`; without this the `++` would be left for `expect_semicolon` to reject. + if let Some(stmt) = try_parse_postfix_incdec(tokens, pos, span)? { + return Ok(stmt); + } // Parse as expression first let expr = parse_expr(tokens, pos)?; diff --git a/src/pipeline.rs b/src/pipeline.rs index c22e98b925..ebf4fd4b2d 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -23,7 +23,7 @@ use crate::span::Span; use crate::source::SourceMode; use crate::timings::CompileTimings; use crate::{ - autoload, codegen, debug_info, errors, exports, ir, ir_lower, ir_passes, lexer, + autoload, codegen, debug_info, errors, exports, func_args, ir, ir_lower, ir_passes, lexer, linker, list_id_prelude, name_resolver, opcache_prelude, optimize, parser, pdo_prelude, resolver, runtime_cache, source_map, tz_prelude, types, var_export_prelude, web_prelude, }; @@ -318,6 +318,25 @@ pub(crate) fn compile(config: CliConfig) { }; timings.record_since("autoload-run", phase_started); + // Desugar PHP's argument-introspection constructs (`func_num_args`, `func_get_args`, + // `func_get_arg`) into plain PHP: every function scope that uses one gains the hidden + // `mixed ...$__elephc_func_args` parameter, so the surplus positional arguments PHP + // allows are collected by the existing variadic machinery. Runs after `autoload::run` + // so autoloaded declarations are covered too — which means call names are already + // resolved here and are matched on their unqualified last segment — and before the AST + // optimizer and the checker, which then only ever see ordinary PHP. + crate::progress::phase("func-args"); + let phase_started = Instant::now(); + let ast = match func_args::desugar(ast) { + Ok(desugared) => desugared, + Err(e) => { + crate::progress::clear(); + errors::report(&e); + process::exit(1); + } + }; + timings.record_since("func-args", phase_started); + // Complete the OPcache script manifest now that all three groups exist, and re-render the // manifest-dependent functions injected above against it. This is a pure substitution of // already-declared, already-name-resolved top-level functions, so it cannot disturb the diff --git a/src/resolver/declarations.rs b/src/resolver/declarations.rs index ed89167e23..af144f76a2 100644 --- a/src/resolver/declarations.rs +++ b/src/resolver/declarations.rs @@ -25,7 +25,7 @@ pub(super) fn extract_discoverable_declarations(stmts: &[Stmt]) -> Vec { let mut context_flushed = false; for stmt in stmts { - let _source_mode = crate::source::scoped_parse_mode(stmt.source_mode); + let _source_mode = crate::source::scoped_parse_mode(stmt.profile()); match &stmt.kind { StmtKind::NamespaceDecl { .. } => { context.clear(); @@ -96,7 +96,7 @@ fn strip_stmts( let mut stripped = Vec::new(); let mut namespace = namespace; for stmt in stmts { - let _source_mode = crate::source::scoped_parse_mode(stmt.source_mode); + let _source_mode = crate::source::scoped_parse_mode(stmt.profile()); let stmt_namespace = namespace.clone(); if let Some(stmt) = strip_stmt( stmt, diff --git a/src/resolver/discovery/stmts.rs b/src/resolver/discovery/stmts.rs index 82d1df5405..d20fa6adc2 100644 --- a/src/resolver/discovery/stmts.rs +++ b/src/resolver/discovery/stmts.rs @@ -44,7 +44,7 @@ pub(super) fn discover_stmts( output: &mut DiscoveryOutput, ) -> Result<(), CompileError> { for stmt in stmts { - let _source_mode = crate::source::scoped_parse_mode(stmt.source_mode); + let _source_mode = crate::source::scoped_parse_mode(stmt.profile()); discover_stmt(stmt, base_dir, loaded_paths, include_chain, state, output)?; } Ok(()) diff --git a/src/resolver/engine.rs b/src/resolver/engine.rs index d1f632121b..442c513f02 100644 --- a/src/resolver/engine.rs +++ b/src/resolver/engine.rs @@ -88,7 +88,7 @@ pub(super) fn resolve_stmts( let mut result = Vec::new(); for stmt in stmts { - let _source_mode = crate::source::scoped_parse_mode(stmt.source_mode); + let _source_mode = crate::source::scoped_parse_mode(stmt.profile()); // Expression-position includes (`$x = require X;` / `return require X;`) are expanded // before generic expression resolution so the included file's statements are inlined into // the caller's scope rather than resolved as an opaque sub-expression. diff --git a/src/source.rs b/src/source.rs index 7f0e2be558..bac6b46c34 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Defines the source-language mode selected from a physical input path. +//! Defines the per-file source profile: the language mode selected from a physical input path +//! plus the `declare(strict_types=1)` state that file opted into. //! Centralizes `.lfc` classification so every file loader agrees on tag and strict-mode semantics. //! //! Called from: @@ -9,6 +10,13 @@ //! Key details: //! - Only `.lfc` opts into tagless elephc source; every other path preserves tagged-PHP behavior. //! - Classification is ASCII case-insensitive and never changes output-path naming. +//! - `strict_types` is a *per-file* PHP directive. It is stamped onto every `Stmt` created while +//! one physical file is parsed (`crate::parser::ast::Stmt::strict_types`) and therefore survives +//! include/autoload merging into the single flat program the type checker sees. Statement +//! rewriting passes must re-install the profile they read off the statement they are rebuilding, +//! which is why `with_parse_mode`/`scoped_parse_mode` take the whole `SourceProfile` instead of +//! the mode alone: a rebuild that dropped the flag would silently downgrade a strict file to +//! PHP's coercive parameter binding. use std::cell::Cell; use std::collections::HashSet; @@ -28,9 +36,37 @@ pub enum SourceMode { Internal, } +/// Everything one physical source file contributes to the AST nodes parsed from it. +/// +/// `mode` comes from the file's path and is known before parsing starts; `strict_types` comes +/// from a `declare(strict_types=1)` directive and is only known once the parser has read the +/// file's first statement. Both are stamped onto every `Stmt` the file produces, so the merged +/// program still answers "which file was this written in" for the two questions that need it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SourceProfile { + /// Language profile selected from the physical path. + pub mode: SourceMode, + /// Whether the file declared `strict_types=1`. + pub strict_types: bool, +} + +impl SourceProfile { + /// Builds the profile a physical file starts parsing with: its path-derived mode and PHP's + /// default coercive typing, which only a `declare(strict_types=1)` directive changes. + pub fn new(mode: SourceMode) -> Self { + Self { + mode, + strict_types: false, + } + } +} + thread_local! { /// Source mode inherited by AST nodes created during one parser invocation. static CURRENT_PARSE_MODE: Cell = const { Cell::new(SourceMode::Internal) }; + + /// `strict_types` state inherited by AST nodes created during one parser invocation. + static CURRENT_STRICT_TYPES: Cell = const { Cell::new(false) }; } impl SourceMode { @@ -79,28 +115,32 @@ pub fn composer_source_stem(component: &str) -> String { .to_string() } -/// RAII guard restoring the parser's previous source mode on drop. +/// RAII guard restoring the parser's previous source profile on drop. pub(crate) struct ParseModeGuard { - previous: SourceMode, + previous: SourceProfile, } impl Drop for ParseModeGuard { - /// Restores the parser source mode active before the nested parse. + /// Restores the parser source profile active before the nested parse. fn drop(&mut self) { - CURRENT_PARSE_MODE.with(|cell| cell.set(self.previous)); + CURRENT_PARSE_MODE.with(|cell| cell.set(self.previous.mode)); + CURRENT_STRICT_TYPES.with(|cell| cell.set(self.previous.strict_types)); } } -/// Runs `f` while parser-created AST nodes inherit `mode`. -pub(crate) fn with_parse_mode(mode: SourceMode, f: impl FnOnce() -> T) -> T { - let _guard = scoped_parse_mode(mode); +/// Runs `f` while parser-created AST nodes inherit `profile`. +pub(crate) fn with_parse_mode(profile: SourceProfile, f: impl FnOnce() -> T) -> T { + let _guard = scoped_parse_mode(profile); f() } -/// Installs one parser/source reconstruction mode until the returned guard is dropped. -pub(crate) fn scoped_parse_mode(mode: SourceMode) -> ParseModeGuard { - let previous = CURRENT_PARSE_MODE.with(|cell| cell.replace(mode)); - ParseModeGuard { previous } +/// Installs one parser/source reconstruction profile until the returned guard is dropped. +pub(crate) fn scoped_parse_mode(profile: SourceProfile) -> ParseModeGuard { + let mode = CURRENT_PARSE_MODE.with(|cell| cell.replace(profile.mode)); + let strict_types = CURRENT_STRICT_TYPES.with(|cell| cell.replace(profile.strict_types)); + ParseModeGuard { + previous: SourceProfile { mode, strict_types }, + } } /// Returns the source mode assigned to AST nodes created at the current parse site. @@ -108,6 +148,21 @@ pub(crate) fn current_parse_mode() -> SourceMode { CURRENT_PARSE_MODE.with(Cell::get) } +/// Returns the `strict_types` state assigned to AST nodes created at the current parse site. +pub(crate) fn current_strict_types() -> bool { + CURRENT_STRICT_TYPES.with(Cell::get) +} + +/// Records that the file currently being parsed declared `strict_types=`. +/// +/// PHP requires the directive to be a file's very first statement, so every statement created +/// after this call belongs to the same file and inherits the flag. The enclosing +/// `ParseModeGuard` resets it when the file's parse ends, which is what keeps the directive from +/// leaking into an included file or back out to the includer. +pub(crate) fn declare_strict_types(enabled: bool) { + CURRENT_STRICT_TYPES.with(|cell| cell.set(enabled)); +} + /// Applies path-dependent post-parse processing shared by every physical source loader. /// /// Magic constants retain the real path, strict PHP audits the unfiltered physical @@ -147,6 +202,47 @@ mod tests { assert_eq!(composer_source_stem("App.lfc"), "App"); } + /// Verifies a nested file parse starts coercive and cannot leak its `strict_types` state + /// back to the includer, which is what makes the directive per-file after include merging. + #[test] + fn nested_parse_scopes_strict_types_to_one_file() { + with_parse_mode(SourceProfile::new(SourceMode::Php), || { + assert!(!current_strict_types()); + declare_strict_types(true); + assert!(current_strict_types()); + + // An `include`d file parses inside the includer's scope and must start coercive. + with_parse_mode(SourceProfile::new(SourceMode::Php), || { + assert!(!current_strict_types()); + declare_strict_types(true); + }); + assert!(current_strict_types()); + + with_parse_mode(SourceProfile::new(SourceMode::Php), || { + assert!(!current_strict_types()); + }); + assert!(current_strict_types()); + }); + assert!(!current_strict_types()); + } + + /// Verifies a statement-rewriting pass re-installing a statement's profile restores both the + /// language mode and the `strict_types` flag, so a rebuilt node keeps its file's binding + /// rules instead of silently reverting to coercive. + #[test] + fn reinstalling_a_profile_restores_both_fields() { + let strict = SourceProfile { + mode: SourceMode::Php, + strict_types: true, + }; + with_parse_mode(strict, || { + assert_eq!(current_parse_mode(), SourceMode::Php); + assert!(current_strict_types()); + }); + assert_eq!(current_parse_mode(), SourceMode::Internal); + assert!(!current_strict_types()); + } + /// Verifies strict PHP applies only to PHP-mode user source. #[test] fn strict_php_is_source_mode_aware() { diff --git a/src/types/array_constants.rs b/src/types/array_constants.rs index 10d9fa6086..2e26bd56a3 100644 --- a/src/types/array_constants.rs +++ b/src/types/array_constants.rs @@ -11,11 +11,14 @@ /// Tuple of `(name, value)` pairs for PHP array integer constants. /// -/// `array_filter()` uses these constants to select which callback arguments are passed. +/// `array_filter()` uses the `ARRAY_FILTER_*` constants to select which callback arguments +/// are passed; `count()` uses the `COUNT_*` constants to select flat or recursive counting. pub(crate) const ARRAY_INT_CONSTANTS: &[(&str, i64)] = &[ ("ARRAY_FILTER_USE_VALUE", 0), ("ARRAY_FILTER_USE_BOTH", 1), ("ARRAY_FILTER_USE_KEY", 2), + ("COUNT_NORMAL", 0), + ("COUNT_RECURSIVE", 1), ]; #[cfg(test)] @@ -32,6 +35,23 @@ mod tests { assert_eq!(entry.1, 0); } + /// Verifies `count()`'s mode constants carry php-src's exact values. + /// + /// `count()`'s omitted-`$mode` default and its `ValueError` range check both assume + /// `COUNT_NORMAL == 0` and `COUNT_RECURSIVE == 1`. + #[test] + fn count_modes_match_php() { + let normal = ARRAY_INT_CONSTANTS + .iter() + .find(|(name, _)| *name == "COUNT_NORMAL") + .expect("COUNT_NORMAL defined"); + let recursive = ARRAY_INT_CONSTANTS + .iter() + .find(|(name, _)| *name == "COUNT_RECURSIVE") + .expect("COUNT_RECURSIVE defined"); + assert_eq!((normal.1, recursive.1), (0, 1)); + } + /// Asserts no duplicate names exist in `ARRAY_INT_CONSTANTS`. #[test] fn no_duplicate_constant_names() { diff --git a/src/types/call_args/mod.rs b/src/types/call_args/mod.rs index 903b73ce80..d5790d588f 100644 --- a/src/types/call_args/mod.rs +++ b/src/types/call_args/mod.rs @@ -20,5 +20,6 @@ pub(crate) use plan::{ }; pub(crate) use planner::{ plan_call_args, plan_call_args_with_regular_param_count_and_assoc_spreads, + validate_no_spread_after_named, }; pub(crate) use static_spread::{expand_static_assoc_spread_args, has_named_args}; diff --git a/src/types/call_args/planner.rs b/src/types/call_args/planner.rs index f881a66c62..21d173747b 100644 --- a/src/types/call_args/planner.rs +++ b/src/types/call_args/planner.rs @@ -8,6 +8,9 @@ //! //! Key details: //! - Source evaluation order is preserved separately from ABI/materialization order for codegen. +//! - Ordering rules that PHP resolves syntactically (spread after a named +//! argument) are checked on the *raw* call-site arguments, before static +//! associative spreads are expanded into named arguments. use crate::parser::ast::{Expr, ExprKind}; use crate::span::Span; @@ -56,6 +59,7 @@ pub(crate) fn plan_call_args_with_regular_param_count( trim_trailing_defaults: bool, allow_unknown_named_variadic: bool, ) -> Result { + validate_no_spread_after_named(args)?; let expanded = expand_static_assoc_spread_args_with_origins(args); let assoc_spread_sources = vec![false; expanded.args.len()]; let (source_args, source_origins, assoc_spread_sources) = @@ -108,6 +112,7 @@ pub(crate) fn plan_call_args_with_regular_param_count_and_assoc_spreads( allow_unknown_named_variadic: bool, assoc_spread_sources: &[bool], ) -> Result { + validate_no_spread_after_named(args)?; let expanded = expand_static_assoc_spread_args_with_origins(args); let expanded_assoc_spread_sources = (0..expanded.args.len()) .map(|idx| assoc_spread_sources.get(idx).copied().unwrap_or(false)) @@ -149,6 +154,37 @@ pub(crate) fn plan_call_args_with_regular_param_count_and_assoc_spreads( ) } +/// Returns `Ok` unless an argument-unpacking (`...`) expression appears after a +/// named argument, in which case it returns `SpreadAfterNamed` for the offending +/// spread. +/// +/// PHP raises "Cannot use argument unpacking after named arguments" while +/// compiling the call, so the shape is rejected regardless of what the unpacked +/// array contains, whether the callee exists, or whether the call is reachable. +/// The check therefore runs on the *raw* call-site arguments: a static +/// string-keyed unpack such as `f(c: 9, ...["a" => 1])` is later rewritten into +/// named arguments by `expand_static_assoc_spread_args_with_origins`, which +/// would otherwise hide the `Spread` node from the ordering check inside +/// `plan_named_call_args`. Only literal `name:` syntax counts as a preceding +/// named argument, matching PHP: `f(...["a" => 1], ...$rest)` stays legal. +/// +/// Exposed so call surfaces whose callee is unknown at compile time (string +/// callables, `new $class(...)`) can enforce the same rule without a signature +/// to plan against; every other surface gets it through `plan_call_args*`. +pub(crate) fn validate_no_spread_after_named(args: &[Expr]) -> Result<(), CallArgPlanError> { + let mut seen_named = false; + for arg in args { + match &arg.kind { + ExprKind::NamedArg { .. } => seen_named = true, + ExprKind::Spread(_) if seen_named => { + return Err(CallArgPlanError::SpreadAfterNamed { span: arg.span }); + } + _ => {} + } + } + Ok(()) +} + /// Returns `Ok` if no positional argument appears after a spread expression, /// otherwise returns `PositionalAfterSpread` for the offending argument. fn validate_positional_spread_order(args: &[Expr]) -> Result<(), CallArgPlanError> { diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index ab1eaf32ce..5580f36550 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -128,6 +128,43 @@ pub(crate) fn array_element_type(arr_ty: &PhpType) -> PhpType { } } +/// Returns the array key type carried by an array/associative-array type. +/// +/// Indexed arrays are integer-keyed; an associative array reports its declared key type. +/// A `Mixed` receiver yields `Mixed` keys so callback validation keeps the declaration as +/// the only available contract. Other non-array types retain the `Int` fallback, matching +/// [`array_element_type`]; callers that require arrays diagnose the container separately. +pub(crate) fn array_key_type(arr_ty: &PhpType) -> PhpType { + match arr_ty { + PhpType::Array(_) => PhpType::Int, + PhpType::AssocArray { key, .. } => (**key).clone(), + PhpType::Mixed => PhpType::Mixed, + _ => PhpType::Int, + } +} + +/// Returns the argument positions whose callback operand is type-checked *contextually* by +/// the builtin's own `check` hook, with parameter types derived from the array element/key. +/// +/// Every eager pre-inference pass must skip these positions. Inferring the closure there +/// first would check its body once with the unhinted `Int`/`Mixed` parameter fallback and +/// reject valid code — e.g. `array_filter($strings, fn($v) => strlen($v) > 5)` would fail +/// with "strlen() argument must be string" before the hook ever supplied the `Str` hint. +/// +/// The table mirrors the `check_array_callback_builtin_call` / `check_callback_builtin_call` +/// call sites in `src/builtins/array/` plus `preg_replace_callback`, whose contextual +/// signature lives in `callables::preg_replace_callback`. +pub(crate) fn contextual_callback_arg_positions(builtin_name: &str) -> &'static [usize] { + match crate::names::php_symbol_key(builtin_name.trim_start_matches('\\')).as_str() { + "array_map" => &[0], + "array_all" | "array_any" | "array_filter" | "array_find" | "array_reduce" + | "array_walk" | "array_walk_recursive" | "preg_replace_callback" | "uasort" + | "uksort" | "usort" => &[1], + "array_udiff" | "array_uintersect" => &[2], + _ => &[], + } +} + /// Prefix for synthetic callback arguments; PHP identifiers cannot begin with a digit. const CALLBACK_ARG_PLACEHOLDER_PREFIX: &str = "0__elephc_callback_arg"; @@ -185,6 +222,28 @@ pub(crate) fn check_array_callback_builtin_call( span: crate::span::Span, env: &TypeEnv, label: &str, +) -> Result { + checker.with_internal_callback_binding(|checker| { + check_array_callback_builtin_call_in_engine_frame( + checker, + callback, + callback_arg_types, + span, + env, + label, + ) + }) +} + +/// Type-checks an array-callback builtin's callback after the caller's `strict_types` setting +/// has been suspended, matching the coercive frame PHP's engine invokes such callbacks from. +fn check_array_callback_builtin_call_in_engine_frame( + checker: &mut Checker, + callback: &Expr, + callback_arg_types: &[PhpType], + span: crate::span::Span, + env: &TypeEnv, + label: &str, ) -> Result { let mut callback_env = env.clone(); let callback_args = callback_arg_types @@ -755,6 +814,28 @@ pub(crate) fn check_callback_builtin_call( span: crate::span::Span, env: &TypeEnv, label: &str, +) -> Result { + checker.with_internal_callback_binding(|checker| { + check_callback_builtin_call_in_engine_frame( + checker, + callback, + callback_args, + span, + env, + label, + ) + }) +} + +/// Type-checks a callback builtin's callback after the caller's `strict_types` setting has been +/// suspended, matching the coercive frame PHP's engine invokes such callbacks from. +fn check_callback_builtin_call_in_engine_frame( + checker: &mut Checker, + callback: &Expr, + callback_args: &[Expr], + span: crate::span::Span, + env: &TypeEnv, + label: &str, ) -> Result { if checker.expr_call_complex_callee_needs_runtime_capture(callback) && !callback_builtin_allows_complex_descriptor_env(label, callback) @@ -1309,12 +1390,38 @@ pub(crate) fn array_filter_callback_arg_types( ) -> Vec { let elem_ty = array_element_type(arr_ty); match mode_arg.and_then(static_array_filter_mode_value) { - Some(1) => vec![elem_ty, PhpType::Int], - Some(2) => vec![PhpType::Int], + Some(1) => vec![elem_ty, array_key_type(arr_ty)], + Some(2) => vec![array_key_type(arr_ty)], _ => vec![elem_ty], } } +/// Returns the contextual callback parameter types for `array_walk()`/`array_walk_recursive()`. +/// +/// PHP always invokes the callback as `callback($value, $key)`, but declaring only the value +/// parameter is legal and common. The key slot is therefore added only when the callback is a +/// closure literal that declares at least two parameters, so a one-parameter callback keeps +/// passing arity validation while `function ($v, $k)` gets its key typed from the array. +pub(crate) fn array_walk_callback_arg_types(arr_ty: &PhpType, callback: &Expr) -> Vec { + let elem_ty = array_element_type(arr_ty); + if callback_declares_at_least_two_params(callback) { + vec![elem_ty, array_key_type(arr_ty)] + } else { + vec![elem_ty] + } +} + +/// Reports whether a callback expression is a closure literal declaring two or more parameters. +/// +/// Only literal closures/arrow functions are inspected; every other callable shape keeps the +/// single-parameter contract the checker can prove without resolving the callable. +fn callback_declares_at_least_two_params(callback: &Expr) -> bool { + match &callback.kind { + ExprKind::Closure { params, .. } => params.len() >= 2, + _ => false, + } +} + /// Returns a compile-time `array_filter()` mode value for integer literals and predefined constants. fn static_array_filter_mode_value(expr: &Expr) -> Option { match &expr.kind { diff --git a/src/types/checker/builtins/language_constructs.rs b/src/types/checker/builtins/language_constructs.rs index a1fe151198..4d16da76f3 100644 --- a/src/types/checker/builtins/language_constructs.rs +++ b/src/types/checker/builtins/language_constructs.rs @@ -11,6 +11,7 @@ use crate::errors::CompileError; use crate::parser::ast::{Expr, ExprKind}; use crate::types::{PhpType, TypeEnv}; +use super::super::null_probe::null_probe_env; use super::super::Checker; /// Type-checks compiler-resident PHP language constructs. @@ -55,7 +56,10 @@ pub(super) fn check( if args.len() != 1 { return Err(CompileError::new(span, "empty() takes exactly 1 argument")); } - checker.infer_type(&args[0], env)?; + // `empty($never)` is legal PHP and answers `true`; the tolerated root is bound + // to `null` for the operand only (see `null_probe`). + let probed = null_probe_env(checker, &args[0], env); + checker.infer_null_probe_operand(&args[0], probed.as_ref().unwrap_or(env))?; Ok(PhpType::Bool) } "unset" => { @@ -81,16 +85,22 @@ pub(super) fn check( } /// Type-checks one `isset()` operand without forcing an observable property read. +/// +/// A never-declared chain root is bound to `null` for the duration of the operand (see +/// [`null_probe_env`]): probing storage that may not exist is exactly what `isset()` is for, and +/// PHP answers `false` rather than warning. fn check_isset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + let probed = null_probe_env(checker, arg, env); + let env = probed.as_ref().unwrap_or(env); if let ExprKind::PropertyAccess { object, .. } | ExprKind::NullsafePropertyAccess { object, .. } = &arg.kind { - let object_ty = checker.infer_type(object, env)?; + let object_ty = checker.infer_null_probe_operand(object, env)?; if isset_object_receiver_type(checker, &object_ty) { return Ok(()); } } - checker.infer_type(arg, env).map(|_| ()) + checker.infer_null_probe_operand(arg, env).map(|_| ()) } /// Returns whether an `isset()` receiver can use non-reading property semantics. @@ -106,16 +116,21 @@ fn isset_object_receiver_type(checker: &Checker, ty: &PhpType) -> bool { } /// Type-checks one `unset()` operand while preserving PHP's non-reading property semantics. +/// +/// Like `isset()`, `unset()` accepts a never-declared chain root (PHP's `unset($never)` +/// is a silent no-op), so the root is bound to `null` for the operand. fn check_unset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + let probed = null_probe_env(checker, arg, env); + let env = probed.as_ref().unwrap_or(env); if let ExprKind::PropertyAccess { object, property } | ExprKind::NullsafePropertyAccess { object, property } = &arg.kind { - let object_ty = checker.infer_type(object, env)?; + let object_ty = checker.infer_null_probe_operand(object, env)?; if unset_object_property_probe_is_valid(checker, &object_ty, property, arg)? { return Ok(()); } } - checker.infer_type(arg, env).map(|_| ()) + checker.infer_null_probe_operand(arg, env).map(|_| ()) } /// Returns true when `unset($object->property)` can be checked without reading the property. diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 678d00948d..70f3c7c6eb 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -28,10 +28,12 @@ pub(crate) use catalog::{ strict_php_hidden_builtin, supported_builtin_function_names_for_profile, }; pub(crate) use callables::{ - array_element_type, array_filter_callback_arg_types, callback_supports_complex_descriptor_env, + array_element_type, array_filter_callback_arg_types, array_key_type, + array_walk_callback_arg_types, callback_supports_complex_descriptor_env, check_array_callback_builtin_call, check_call_user_func, check_call_user_func_array, - check_callback_builtin_call, check_function_exists, + check_function_exists, check_preg_replace_callback_first_class_call, + contextual_callback_arg_positions, runtime_callable_array_type, }; @@ -188,7 +190,15 @@ impl Checker { unreachable!("non-checker builtin returned from semantic validation branch"); }; if !lazy { - for arg in args.iter() { + // A contextual callback position is deliberately left to the hook, which + // types the closure's unannotated parameters from the array element/key + // before checking its body. Pre-inferring it here would check that body + // once against the unhinted parameter fallback and reject valid PHP. + let contextual = contextual_callback_arg_positions(name); + for (idx, arg) in args.iter().enumerate() { + if contextual.contains(&idx) { + continue; + } self.infer_type(arg, env)?; } } diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 8d7cf2767a..90d82d06e0 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -16,9 +16,11 @@ use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::ent_constants::ENT_INT_CONSTANTS; use crate::types::error_constants::ERROR_LEVEL_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; +use crate::types::math_constants::MATH_INT_CONSTANTS; use crate::types::session_constants::SESSION_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; use crate::types::stream_constants::STREAM_INT_CONSTANTS; +use crate::types::string_constants::STRING_INT_CONSTANTS; use crate::types::PhpType; use super::super::Checker; @@ -92,9 +94,15 @@ impl Checker { for (name, _value) in JSON_INT_CONSTANTS { constants.insert((*name).to_string(), PhpType::Int); } + for (name, _value) in MATH_INT_CONSTANTS { + constants.insert((*name).to_string(), PhpType::Int); + } for (name, _value) in STREAM_INT_CONSTANTS { constants.insert((*name).to_string(), PhpType::Int); } + for (name, _value) in STRING_INT_CONSTANTS { + constants.insert((*name).to_string(), PhpType::Int); + } for (name, _value) in PREG_INT_CONSTANTS { constants.insert((*name).to_string(), PhpType::Int); } @@ -137,6 +145,7 @@ impl Checker { callable_sigs: HashMap::new(), callable_param_names: HashSet::new(), callable_param_sigs: HashMap::new(), + strict_types: false, param_specialization_seen: HashSet::new(), callable_return_sigs: HashMap::new(), callable_array_return_sigs: HashMap::new(), @@ -168,6 +177,10 @@ impl Checker { active_statics: HashSet::new(), foreach_key_locals: HashSet::new(), eval_barrier_active: false, + flow_typed_returns: HashMap::new(), + null_probe_scope_is_top_level: false, + pending_null_probe_roots: Vec::new(), + null_probe_depth: 0, break_continue_depth: 0, finally_break_continue_bases: Vec::new(), current_loop_storage_scope: "main".to_string(), @@ -176,6 +189,7 @@ impl Checker { throw_access_sites: HashMap::new(), builtin_call_types: HashMap::new(), loop_storage_types: HashMap::new(), + string_incdec_locals: HashSet::new(), } } } diff --git a/src/types/checker/driver/top_level.rs b/src/types/checker/driver/top_level.rs index c927a26208..41e62e0f0b 100644 --- a/src/types/checker/driver/top_level.rs +++ b/src/types/checker/driver/top_level.rs @@ -12,6 +12,7 @@ use std::collections::HashMap; use crate::errors::CompileError; use crate::parser::ast::Program; +use crate::span::Span; use crate::types::{PhpType, TypeEnv}; use super::super::Checker; @@ -29,22 +30,78 @@ impl Checker { ) -> (TypeEnv, Vec>) { let saved_eval_barrier_active = self.eval_barrier_active; self.eval_barrier_active = false; + let saved_null_probe_scope = self.null_probe_scope_is_top_level; + self.null_probe_scope_is_top_level = true; + self.pending_null_probe_roots.clear(); let mut global_env = self.seed_global_env(); let mut all_errors = Vec::with_capacity(program.len()); - for stmt in program { + // `(statement index, name, span)` for every null probe this pass tolerated, so the + // deferred diagnostic lands on the statement that contains the probe. + let mut probe_roots: Vec<(usize, String, Span)> = Vec::new(); + for (index, stmt) in program.iter().enumerate() { self.top_level_env = global_env.clone(); let stmt_errors = self .check_stmt(stmt, &mut global_env) .err() .map(|error| error.flatten()) .unwrap_or_default(); + probe_roots.extend( + self.pending_null_probe_roots + .drain(..) + .map(|(name, span)| (index, name, span)), + ); all_errors.push(stmt_errors); } + self.resolve_null_probe_roots(probe_roots, &mut global_env, &mut all_errors); self.top_level_env = global_env.clone(); self.eval_barrier_active = saved_eval_barrier_active; + self.null_probe_scope_is_top_level = saved_null_probe_scope; (global_env, all_errors) } + /// Decides, with the finished `global_env` in hand, whether each tolerated null-probe root + /// was legitimate. + /// + /// A name still absent from `global_env` was never assigned anywhere at top level, so it is + /// `null` for the whole scope: binding it to `PhpType::Void` both matches PHP and gives EIR + /// lowering a slot type it can answer `isset`/`empty`/`??` from without reading storage that + /// no store ever initializes. A name that *is* bound was assigned somewhere in the same + /// scope, so its slot carries that assigned type and the probe would read it before the + /// store — the original `Undefined variable` diagnostic is restored for those. + fn resolve_null_probe_roots( + &mut self, + probe_roots: Vec<(usize, String, Span)>, + global_env: &mut TypeEnv, + all_errors: &mut [Vec], + ) { + let mut reported: std::collections::HashSet<(String, u32, u32)> = + std::collections::HashSet::new(); + for (index, name, span) in probe_roots { + match global_env.get(&name) { + // Never assigned at top level: seed the `null` binding lowering needs. The same + // name can be probed several times (and each probe is seen by both the + // assignment-effect walk and expression inference), so an already-seeded `Void` + // is just a repeat of this decision and stays accepted. + None => { + global_env.insert(name, PhpType::Void); + } + Some(PhpType::Void) => {} + Some(_) => { + // One probe is visited by both the assignment-effect walk and expression + // inference, so the same (name, position) can arrive several times. + if !reported.insert((name.clone(), span.line, span.col)) { + continue; + } + if let Some(stmt_errors) = all_errors.get_mut(index) { + stmt_errors.push( + super::super::null_probe::unrepresentable_probe_root_error(&name, span), + ); + } + } + } + } + } + /// Determines whether top-level errors for a statement can be suppressed. /// /// Only reached when the final fixpoint pass produced no error for this statement, so any diff --git a/src/types/checker/functions.rs b/src/types/checker/functions.rs index eac17f847b..eff3787c8d 100644 --- a/src/types/checker/functions.rs +++ b/src/types/checker/functions.rs @@ -10,5 +10,6 @@ //! - User functions, builtins, externs, and callable aliases must share the same argument semantics. mod call_validation; +mod param_binding; mod resolution; mod returns; diff --git a/src/types/checker/functions/call_validation.rs b/src/types/checker/functions/call_validation.rs index ea5bb4e136..e05c98329a 100644 --- a/src/types/checker/functions/call_validation.rs +++ b/src/types/checker/functions/call_validation.rs @@ -58,13 +58,9 @@ fn call_arg_plan_error( callee_desc ), ), - CallArgPlanError::SpreadAfterNamed { span } => CompileError::new( - span, - &format!( - "{} cannot use argument unpacking after named arguments", - callee_desc - ), - ), + CallArgPlanError::SpreadAfterNamed { span } => { + spread_after_named_error(span, callee_desc) + } CallArgPlanError::MissingRequired { span, param_idx } => { let param_name = sig .params @@ -79,6 +75,19 @@ fn call_arg_plan_error( } } +/// Builds the diagnostic for PHP's compile-time +/// "Cannot use argument unpacking after named arguments" fatal, prefixed with +/// the callee description used by the rest of the call diagnostics. +fn spread_after_named_error(span: crate::span::Span, callee_desc: &str) -> CompileError { + CompileError::new( + span, + &format!( + "{} cannot use argument unpacking after named arguments", + callee_desc + ), + ) +} + /// Returns a boolean vector indicating which argument positions contain assoc-spread sources /// (arrays with string keys that map to named arguments after spread expansion). fn assoc_spread_sources(args: &[Expr], env: &TypeEnv) -> Vec { @@ -105,6 +114,35 @@ fn is_assoc_spread_source(expr: &Expr, env: &TypeEnv) -> bool { } impl Checker { + /// Enforces PHP's syntactic "no argument unpacking after named arguments" + /// rule on a call surface whose callee is not resolvable at compile time + /// (string callables, `new $class(...)`), where no signature is available to + /// run the shared planner against. + /// + /// The rule itself stays in `crate::types::call_args`; this only maps its + /// error onto the same diagnostic the planner-backed surfaces produce. + pub(crate) fn require_no_spread_after_named_args( + &self, + args: &[Expr], + callee_desc: &str, + ) -> Result<(), CompileError> { + call_args::validate_no_spread_after_named(args).map_err(|err| match err { + CallArgPlanError::SpreadAfterNamed { span } => { + spread_after_named_error(span, callee_desc) + } + // The rule only reports `SpreadAfterNamed`; the remaining variants + // need a signature to plan against and cannot originate here. Keep + // the match exhaustive so a future rule still reports a real span. + CallArgPlanError::UnknownNamed { span, .. } + | CallArgPlanError::Duplicate { span, .. } + | CallArgPlanError::PositionalAfterNamed { span } + | CallArgPlanError::PositionalAfterSpread { span } + | CallArgPlanError::MissingRequired { span, .. } => { + CompileError::new(span, &format!("{} has invalid arguments", callee_desc)) + } + }) + } + /// Returns true when an argument expression is an l-value supported by by-reference calls. pub(crate) fn is_by_ref_argument_lvalue( &mut self, @@ -125,6 +163,10 @@ impl Checker { /// Normalizes arguments for a user-defined function call, allowing unknown named arguments /// to be collected into the variadic parameter. + /// + /// The one exception is the hidden variadic added by `crate::func_args` to collect the + /// surplus positional arguments `func_get_args()` exposes: the callee declares no + /// variadic of its own, so PHP still rejects an unknown named argument there. pub(crate) fn normalize_named_call_args( &self, sig: &FunctionSig, @@ -133,7 +175,16 @@ impl Checker { callee_desc: &str, env: &TypeEnv, ) -> Result, CompileError> { - self.normalize_call_args(sig, args, span, callee_desc, false, true, env) + let allow_unknown_named_variadic = !crate::func_args::sig_collects_surplus_args(sig); + self.normalize_call_args( + sig, + args, + span, + callee_desc, + false, + allow_unknown_named_variadic, + env, + ) } /// Normalizes arguments for a builtin or extern function call, rejecting unknown named @@ -263,9 +314,73 @@ impl Checker { caller_env, callee_desc, false, + false, + ) + } + + /// Validates a direct call to a method or constructor of `owner_class`, applying PHP's + /// coercive parameter binding when that class is declared in user source. + /// + /// Only surfaces whose arguments reach EIR through `lower_args_with_signature` may opt in: + /// that is where the matching argument rewrite runs, and accepting a binding without it + /// would hand raw storage to a differently typed parameter slot. Compiler-injected classes + /// (SPL, `Exception`, reflection, …) lower several of their members through bespoke + /// emitters instead, so they stay on the strict path. + pub(crate) fn check_user_declared_call( + &mut self, + sig: &FunctionSig, + args: &[Expr], + span: crate::span::Span, + caller_env: &TypeEnv, + callee_desc: &str, + owner_class: &str, + ) -> Result { + let coercive = self.class_is_user_declared(owner_class); + self.check_known_callable_call_with_options( + sig, + args, + span, + caller_env, + callee_desc, + false, + coercive, ) } + /// `check_user_declared_call` for a callee that also accepts spread arguments into + /// by-reference parameters materialized by descriptor invokers. + pub(crate) fn check_user_declared_call_allowing_by_ref_spread( + &mut self, + sig: &FunctionSig, + args: &[Expr], + span: crate::span::Span, + caller_env: &TypeEnv, + callee_desc: &str, + owner_class: &str, + ) -> Result { + let coercive = self.class_is_user_declared(owner_class); + self.check_known_callable_call_with_options( + sig, + args, + span, + caller_env, + callee_desc, + true, + coercive, + ) + } + + /// Returns true when `class_name` is a class-like symbol declared in user source. + /// + /// Compiler-injected classes carry `Span::dummy()` as their declaration span, which is the + /// only marker distinguishing them from user declarations. An unknown name is treated as + /// not user-declared so an unresolved receiver never silently gains coercive binding. + fn class_is_user_declared(&self, class_name: &str) -> bool { + self.classes + .get(class_name) + .is_some_and(|info| info.declaration_span != crate::span::Span::dummy()) + } + /// Validates a known callable call while allowing spread arguments for by-reference /// parameters that will be materialized by descriptor invokers at runtime. pub(crate) fn check_known_callable_call_allowing_by_ref_spread( @@ -283,10 +398,14 @@ impl Checker { caller_env, callee_desc, true, + false, ) } /// Shared implementation for known callable call validation. + /// + /// `coercive_param_binding` opts the callee into PHP's coercive parameter binding for its + /// declared parameters; see `check_user_declared_call` for when that is sound. fn check_known_callable_call_with_options( &mut self, sig: &FunctionSig, @@ -295,6 +414,7 @@ impl Checker { caller_env: &TypeEnv, callee_desc: &str, allow_by_ref_spread: bool, + coercive_param_binding: bool, ) -> Result { let normalized_args = self.normalize_named_call_args(sig, args, span, callee_desc, caller_env)?; let args = normalized_args.as_slice(); @@ -392,16 +512,54 @@ impl Checker { &format!("{} parameter ${}", callee_desc, param_name), )?; } - self.require_compatible_arg_type( + // `strict_types` applies to every declared parameter type, including the + // closure and first-class-callable surfaces that stay off the coercive + // path. Builtin signatures carry `declared_params: false` throughout + // (`crate::builtins::registry`), so this never fires for an internal + // function whose parameter types the checker does not consume. + if sig.declared_params.get(param_idx).copied().unwrap_or(false) { + self.require_strict_types_param_binding( + expected_ty, + &actual_ty, + arg.span, + &format!("{} parameter ${}", callee_desc, param_name), + )?; + } + if coercive_param_binding + && sig.declared_params.get(param_idx).copied().unwrap_or(false) + { + self.require_bound_param_arg_type( + expected_ty, + &actual_ty, + arg, + caller_env, + &format!("{} parameter ${}", callee_desc, param_name), + None, + sig.ref_params.get(param_idx).copied().unwrap_or(false), + )?; + } else { + self.require_compatible_arg_type( + expected_ty, + &actual_ty, + arg.span, + &format!("{} parameter ${}", callee_desc, param_name), + )?; + } + } + } else if let (Some(vname), Some(expected_ty)) = + (sig.variadic.as_ref(), variadic_elem_ty.as_ref()) + { + // The variadic occupies the last `declared_params` slot, so gating on it keeps + // the strict rejection off builtin variadics, whose registry-derived parameter + // types the checker does not otherwise consume. + if sig.declared_params.last().copied().unwrap_or(false) { + self.require_strict_types_param_binding( expected_ty, &actual_ty, arg.span, - &format!("{} parameter ${}", callee_desc, param_name), + &format!("{} variadic parameter ${}", callee_desc, vname), )?; } - } else if let (Some(vname), Some(expected_ty)) = - (sig.variadic.as_ref(), variadic_elem_ty.as_ref()) - { self.require_compatible_arg_type( expected_ty, &actual_ty, diff --git a/src/types/checker/functions/param_binding.rs b/src/types/checker/functions/param_binding.rs new file mode 100644 index 0000000000..b3c6dd76e0 --- /dev/null +++ b/src/types/checker/functions/param_binding.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Applies PHP's parameter-binding rules when a declared user-defined parameter is handed an +//! argument whose inferred type does not already satisfy it: coercive scalar binding +//! (`string $s` accepting `42`) and callable-name strings (`callable $f` accepting +//! `"strtoupper"`). +//! +//! Called from: +//! - `crate::types::checker::functions::resolution` (user function and method calls) +//! +//! Key details: +//! - The accept/reject decision lives in `crate::types::param_binding`, shared with the +//! matching EIR argument rewrite. This file only turns that decision into a diagnostic and +//! registers the callable signature a bound callable string implies. +//! - Coercive binding runs *after* `types_compatible` / `type_accepts` have already failed, so +//! it can only widen what is accepted, never narrow it. +//! - The `declare(strict_types=1)` rejection runs *before* them instead, because the widenings +//! PHP drops in strict mode (`bool`→`int`, `int`→`bool`, …) are ones `types_compatible` +//! already accepts on its own. + +use crate::errors::CompileError; +use crate::parser::ast::Expr; +use crate::span::Span; +use crate::types::param_binding::{ + classify_param_binding, strict_param_binding_rejection, ParamBinding, +}; +use crate::types::{FunctionSig, PhpType, TypeEnv}; + +use super::super::Checker; + +impl Checker { + /// Validates one declared parameter against an argument, allowing PHP's coercive + /// parameter binding and callable-name strings before reporting a type mismatch. + /// + /// `owner` names the `(function, parameter)` pair used to register the signature of a + /// callable-name string, so the callee can type-check invocations of that parameter the + /// same way it does for a first-class callable. Pass `None` where that registration does + /// not apply (variadic elements, spread-expanded positions). + /// + /// `by_ref` must be true for a pass-by-reference parameter. PHP coerces those in place and + /// writes the converted value back to the caller's variable; elephc's binding produces a + /// temporary instead, so a by-reference parameter stays on the strict path rather than + /// silently dropping the callee's writes. + /// + /// When the call site's file declared `strict_types=1`, the strict rejection runs first and + /// no coercive binding is considered at all. + /// + /// # Errors + /// Returns the standard ` expects , got ` mismatch, extended + /// with the PHP behaviour elephc cannot reproduce when a binding rule exists but is not + /// statically decidable. + pub(crate) fn require_bound_param_arg_type( + &mut self, + expected: &PhpType, + actual: &PhpType, + arg: &Expr, + env: &TypeEnv, + context: &str, + owner: Option<(&str, &str)>, + by_ref: bool, + ) -> Result<(), CompileError> { + self.require_strict_types_param_binding(expected, actual, arg.span, context)?; + if Self::types_compatible(expected, actual) || self.type_accepts(expected, actual) { + return Ok(()); + } + if by_ref { + return self.require_compatible_arg_type(expected, actual, arg.span, context); + } + match classify_param_binding(expected, actual, arg) { + ParamBinding::Identity | ParamBinding::Cast(_) | ParamBinding::Const(_) => Ok(()), + ParamBinding::Callable(target) => { + let sig = self + .resolve_first_class_callable_sig(&target, arg.span, env) + .map_err(|err| { + Self::param_binding_error( + expected, + actual, + arg, + context, + err.message.as_str(), + ) + })?; + self.register_bound_callable_param_sig(owner, sig); + Ok(()) + } + ParamBinding::Deprecated(detail) + | ParamBinding::TypeError(detail) + | ParamBinding::NeedsRuntimeCheck(detail) => Err(Self::param_binding_error( + expected, actual, arg, context, &detail, + )), + ParamBinding::Rejected => { + self.require_compatible_arg_type(expected, actual, arg.span, context) + } + } + } + + /// Rejects a declared-parameter argument that `declare(strict_types=1)` forbids at this + /// call site. + /// + /// No-op unless the statement being checked came from a file that declared the directive, + /// and no-op for every conversion PHP still performs in strict mode (the `int`→`float` + /// widening and every non-scalar declared type). It is called from the coercive binding + /// path *and* from the call surfaces that never had coercive binding — declared variadic + /// element types and the sig-based closure/first-class-callable path — so the directive + /// reaches every declared parameter, not only the coercively bound ones. + /// + /// # Errors + /// Returns the standard ` expects , got ` mismatch extended with + /// the `TypeError` PHP would throw at run time. + pub(crate) fn require_strict_types_param_binding( + &self, + expected: &PhpType, + actual: &PhpType, + span: Span, + context: &str, + ) -> Result<(), CompileError> { + if !self.strict_types { + return Ok(()); + } + match strict_param_binding_rejection(expected, actual) { + Some(detail) => Err(Self::binding_mismatch_error( + expected, actual, span, context, &detail, + )), + None => Ok(()), + } + } + + /// Runs `operation` with `declare(strict_types=1)` parameter binding suspended. + /// + /// PHP invokes a callback handed to an internal function (`array_map`, `usort`, + /// `array_walk`, `preg_replace_callback`, …) from the engine's own frame, which is never + /// strict, so the calling file's directive does not reach the callback's parameters — + /// `array_map('g', [true])` still coerces `true` into `g(int $i)` under `strict_types=1`. + /// `call_user_func`/`call_user_func_array` are the documented exception: they forward the + /// caller's frame and therefore stay on the strict path. Verified on PHP 8.4.20. + pub(crate) fn with_internal_callback_binding( + &mut self, + operation: impl FnOnce(&mut Self) -> T, + ) -> T { + let outer_strict_types = self.strict_types; + self.strict_types = false; + let result = operation(self); + self.strict_types = outer_strict_types; + result + } + + /// Builds the extended parameter mismatch diagnostic, appending the PHP behaviour that + /// explains why elephc refuses the binding. + fn param_binding_error( + expected: &PhpType, + actual: &PhpType, + arg: &Expr, + context: &str, + detail: &str, + ) -> CompileError { + Self::binding_mismatch_error(expected, actual, arg.span, context, detail) + } + + /// Formats one parameter-binding rejection at `span`, shared by the coercive and strict + /// paths so both diagnostics read identically apart from the explanation. + fn binding_mismatch_error( + expected: &PhpType, + actual: &PhpType, + span: Span, + context: &str, + detail: &str, + ) -> CompileError { + CompileError::new( + span, + &format!( + "{} expects {:?}, got {:?} — {}", + context, expected, actual, detail + ), + ) + } + + /// Records the signature a bound callable-name string gives a declared `callable` + /// parameter, so the callee resolves `$f(...)` exactly as it does for a first-class + /// callable argument. + fn register_bound_callable_param_sig( + &mut self, + owner: Option<(&str, &str)>, + sig: FunctionSig, + ) { + let Some((owner_name, param_name)) = owner else { + return; + }; + let key = (owner_name.to_string(), param_name.to_string()); + if self.callable_param_sigs.get(&key) != Some(&sig) { + self.callable_param_sigs.insert(key, sig); + } + } +} diff --git a/src/types/checker/functions/resolution/call.rs b/src/types/checker/functions/resolution/call.rs index 3d3df5a56e..1c03c0612c 100644 --- a/src/types/checker/functions/resolution/call.rs +++ b/src/types/checker/functions/resolution/call.rs @@ -361,11 +361,14 @@ impl Checker { &format!("Function '{}' parameter ${}", name, param_name), )?; } - self.require_compatible_arg_type( + self.require_bound_param_arg_type( &declared_ty, &ty, - arg.span, + arg, + caller_env, &format!("Function '{}' parameter ${}", name, param_name), + Some((name, decl.params[arg_idx].as_str())), + decl.ref_params.get(arg_idx).copied().unwrap_or(false), )?; let specialized_ty = Self::specialize_generic_array_param_hint(&declared_ty, &ty); @@ -391,6 +394,15 @@ impl Checker { arg.span, &format!("Function '{}' variadic parameter ${}", name, vname), )?; + // PHP applies `strict_types` to a variadic element exactly like a regular + // declared parameter, so the strict rejection runs here too; the coercive + // widenings `require_compatible_arg_type` allows are unchanged otherwise. + self.require_strict_types_param_binding( + &elem_ty, + &ty, + arg.span, + &format!("Function '{}' variadic parameter ${}", name, vname), + )?; } arg_idx += 1; } diff --git a/src/types/checker/functions/resolution/resolved.rs b/src/types/checker/functions/resolution/resolved.rs index 3be5285802..91a100c2b9 100644 --- a/src/types/checker/functions/resolution/resolved.rs +++ b/src/types/checker/functions/resolution/resolved.rs @@ -156,12 +156,33 @@ impl Checker { &format!("Function '{}' parameter ${}", name, param_name), )?; } - self.require_compatible_arg_type( - expected_ty, - &actual_ty, - arg.span, - &format!("Function '{}' parameter ${}", name, param_name), - )?; + // PHP's parameter binding only applies to a *declared* parameter type. + // An inferred parameter's "expected" type is just what earlier call sites + // produced, so coercing against it would invent a conversion PHP does not + // perform. + if effective_sig + .declared_params + .get(param_idx) + .copied() + .unwrap_or(false) + { + self.require_bound_param_arg_type( + expected_ty, + &actual_ty, + arg, + caller_env, + &format!("Function '{}' parameter ${}", name, param_name), + Some((name, param_name.as_str())), + effective_sig.ref_params.get(param_idx).copied().unwrap_or(false), + )?; + } else { + self.require_compatible_arg_type( + expected_ty, + &actual_ty, + arg.span, + &format!("Function '{}' parameter ${}", name, param_name), + )?; + } } } else if let (Some(vname), Some(expected_ty)) = (effective_sig.variadic.as_ref(), variadic_elem_ty.as_ref()) diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index c6a4871387..008db8c3dd 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -323,7 +323,11 @@ impl Checker { /// Returns true when a declared generator return annotation accepts /// the actual `Generator` object returned when the body contains `yield`. - fn generator_return_type_accepts(&self, declared_ret: &PhpType) -> bool { + /// + /// Shared with the method pass (`crate::types::checker::method_pass`) so a + /// generator method's hint is validated by exactly the same rule as a + /// generator function's. + pub(crate) fn generator_return_type_accepts(&self, declared_ret: &PhpType) -> bool { if matches!(declared_ret, PhpType::Object(name) if name == "Traversable") { return true; } diff --git a/src/types/checker/functions/returns.rs b/src/types/checker/functions/returns.rs index c080abfebd..52fff9694a 100644 --- a/src/types/checker/functions/returns.rs +++ b/src/types/checker/functions/returns.rs @@ -62,7 +62,16 @@ impl Checker { ) { match &stmt.kind { StmtKind::Return(Some(expr)) => { - if let Ok(ty) = self.infer_type(expr, env) { + // Prefer the type recorded while this exact statement was checked: it reflects + // the environment at the return site, whereas `env` here is the body's final + // environment and would leak a later narrowing backwards. Falls back to + // re-inference when nothing was recorded (e.g. an unchecked body). + let recorded = self + .flow_typed_returns + .get(&(stmt as *const Stmt as usize)) + .filter(|(span, _)| *span == stmt.span) + .map(|(_, ty)| ty.clone()); + if let Some(ty) = recorded.or_else(|| self.infer_type(expr, env).ok()) { returns.push(ReturnInfo { ty, has_value: true, diff --git a/src/types/checker/inference/expr/basic.rs b/src/types/checker/inference/expr/basic.rs index b9eddf9850..a113d92cf2 100644 --- a/src/types/checker/inference/expr/basic.rs +++ b/src/types/checker/inference/expr/basic.rs @@ -64,12 +64,22 @@ impl Checker { ExprKind::PreIncrement(name) | ExprKind::PreDecrement(name) => match env.get(name) { Some(PhpType::Int) => Ok(PhpType::Mixed), Some(PhpType::Mixed) => Ok(PhpType::Mixed), + // PHP's string increment can change the value's type (`"9"++` is + // `int(10)`), so the pre-form's value is dynamically tagged. EIR lowering + // gives the local boxed Mixed frame storage for the same reason. + Some(PhpType::Str) => { + self.reject_unboxable_string_incdec(name, expr.span)?; + self.record_string_incdec_local(name); + Ok(PhpType::Mixed) + } + // PHP's `++`/`--` on a float adds or subtracts 1.0 and keeps the float. + Some(PhpType::Float) => Ok(PhpType::Float), Some(PhpType::Bool) | Some(PhpType::False) | Some(PhpType::Void) => { Ok(PhpType::Int) } Some(other) => Err(CompileError::new( expr.span, - &format!("Cannot increment/decrement ${} of type {:?}", name, other), + &increment_type_error(name, other), )), None => Err(CompileError::new( expr.span, @@ -82,9 +92,19 @@ impl Checker { | Some(PhpType::False) | Some(PhpType::Void) => Ok(PhpType::Int), Some(PhpType::Mixed) => Ok(PhpType::Mixed), + // The post-forms yield the value the local held BEFORE the update, so a + // string local still answers `string` even though the update itself can + // retype the local (see the pre-form arm). + Some(PhpType::Str) => { + self.reject_unboxable_string_incdec(name, expr.span)?; + self.record_string_incdec_local(name); + Ok(PhpType::Str) + } + // The post-forms yield the float the local held before the update. + Some(PhpType::Float) => Ok(PhpType::Float), Some(other) => Err(CompileError::new( expr.span, - &format!("Cannot increment/decrement ${} of type {:?}", name, other), + &increment_type_error(name, other), )), None if self.eval_barrier_active => Ok(PhpType::Int), None => Err(CompileError::new( @@ -303,6 +323,9 @@ impl Checker { // "undefined index" warning behavior for this very // common idiom (e.g. `json_decode($json, true)["k"]`). PhpType::Mixed => Ok(PhpType::Mixed), + // `isset($n['k'])` / `$n['k'] ?? $d` reach through a null base in PHP and + // answer `false` / the default; only a probe context may do so. + PhpType::Void if self.null_probe_depth > 0 => Ok(PhpType::Void), _ => Err(CompileError::new(expr.span, "Cannot index non-array")), } } @@ -375,3 +398,11 @@ impl Checker { } } } + +/// Formats the diagnostic for `++`/`--` applied to a local elephc cannot update in place. +/// +/// `int`, `float`, `bool`, `null`, `string`, and boxed `mixed` locals all have an increment +/// path; everything else (arrays, objects, buffers, pointers) reaches this diagnostic. +fn increment_type_error(name: &str, ty: &PhpType) -> String { + format!("Cannot increment/decrement ${} of type {:?}", name, ty) +} diff --git a/src/types/checker/inference/expr/calls_objects.rs b/src/types/checker/inference/expr/calls_objects.rs index dc6c94795c..7cbaf5195b 100644 --- a/src/types/checker/inference/expr/calls_objects.rs +++ b/src/types/checker/inference/expr/calls_objects.rs @@ -60,7 +60,14 @@ impl Checker { Ok(PhpType::Int) } ExprKind::NullCoalesce { value, default } => { - let vt = self.infer_type(value, env)?; + // `??` is a null probe: PHP evaluates `$neverDefined ?? $d` to `$d` without an + // undefined-variable warning, so a never-declared chain root reads as `null` + // here. The default operand keeps ordinary inference. + let probed = + crate::types::checker::null_probe::null_probe_env(self, value, env); + let probed_env = probed.clone(); + let vt = self + .infer_null_probe_operand(value, probed_env.as_ref().unwrap_or(env))?; let dt = self.infer_type(default, env)?; let non_null_value = if Self::union_contains_void(&vt) { self.strip_void_from_union(&vt) @@ -173,6 +180,9 @@ impl Checker { // object's type. Infer the name expression for its side // effects + warnings, type-check the args generically, and // return Mixed. + // The unpack-after-named shape is syntactic in + // PHP, so it is still rejected without a known constructor. + self.require_no_spread_after_named_args(args, "Dynamic constructor")?; self.infer_type(name_expr, env)?; for arg in args { self.infer_type(arg, env)?; @@ -209,7 +219,7 @@ impl Checker { self.infer_dynamic_property_access_type(object, property, expr, env, true) } ExprKind::StaticPropertyAccess { receiver, property } => { - self.infer_static_property_access_type(receiver, property, expr) + self.infer_static_property_access_type(receiver, property, expr, env) } ExprKind::MethodCall { object, @@ -313,10 +323,18 @@ impl Checker { } ExprKind::YieldFrom(inner) => { let inner_ty = self.infer_type(inner, env)?; + // `yield from` over an array is desugared by EIR lowering into an + // iterator loop that re-yields every key/value pair, and that loop + // handles indexed and keyed literals alike (`lower_yield_from_array` + // dispatches on `Array` *and* `AssocArray`). Accept a keyed literal + // (`[5 => "x", "s" => "y"]`, `[$i * 10 => "L"]`) the same way an + // indexed one is accepted; PHP accepts both. let supported = match &inner.kind { - ExprKind::ArrayLiteral(_) => true, + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => true, ExprKind::FunctionCall { .. } | ExprKind::Variable(_) => { - self.type_accepts(&PhpType::Object("Generator".to_string()), &inner_ty) + matches!(inner_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) + || self + .type_accepts(&PhpType::Object("Generator".to_string()), &inner_ty) } _ => false, }; diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 60e14b4bb6..f87839ce2d 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -13,6 +13,7 @@ use crate::names::php_symbol_key; use crate::parser::ast::{BinOp, CallableTarget, Expr, ExprKind}; use crate::types::{PhpType, TypeEnv}; +use super::super::super::null_probe; use super::super::super::Checker; use super::{merge_match_arm_result_type, merge_null_coalesce_result_type}; @@ -81,7 +82,9 @@ impl Checker { ExprKind::PreIncrement(name) | ExprKind::PreDecrement(name) => { let old_ty = env.get(name).cloned(); let result_ty = self.infer_type(expr, env)?; - if matches!(old_ty, Some(PhpType::Int)) { + // `int` can overflow to float and `string` can become int/float + // (`"9"++` is `int(10)`), so the local is dynamically typed afterwards. + if matches!(old_ty, Some(PhpType::Int) | Some(PhpType::Str)) { env.insert(name.clone(), PhpType::Mixed); } Ok(result_ty) @@ -89,7 +92,9 @@ impl Checker { ExprKind::PostIncrement(name) | ExprKind::PostDecrement(name) => { let old_ty = env.get(name).cloned(); let result_ty = self.infer_type(expr, env)?; - if matches!(old_ty, Some(PhpType::Int)) { + // Same retype as the pre-form: only the RESULT differs, and it was already + // computed above against the type the local held before the update. + if matches!(old_ty, Some(PhpType::Int) | Some(PhpType::Str)) { env.insert(name.clone(), PhpType::Mixed); } Ok(result_ty) @@ -107,7 +112,12 @@ impl Checker { } } ExprKind::NullCoalesce { value, default } => { - let value_ty = self.infer_type_with_assignment_effects(value, env)?; + // `$neverDefined ?? $d` is a null probe: PHP answers `$d` without an + // undefined-variable warning, so the left chain root reads as `null`. + let probe = null_probe::begin_null_probe_root(self, value, env); + let value_ty = self.infer_null_probe_operand_with_effects(value, env); + null_probe::end_null_probe_root(probe, env); + let value_ty = value_ty?; let default_ty = if value_ty == PhpType::Void { self.infer_type_with_assignment_effects(default, env)? } else { @@ -228,27 +238,41 @@ impl Checker { php_symbol_key(builtin_name).as_str(), "isset" | "unset" ) { + // `isset($never)` / `unset($never)` are exactly the constructs PHP provides + // for probing storage that may never have been declared, so a never-declared + // chain root reads as `null` for the operand. for arg in &expanded_args { - self.infer_non_reading_arg_assignment_effects(arg, env)?; + let probe = null_probe::begin_null_probe_root(self, arg, env); + let effects = self.infer_non_reading_arg_assignment_effects(arg, env); + null_probe::end_null_probe_root(probe, env); + effects?; } } else if !builtin_name.eq_ignore_ascii_case("unset") { + // `empty()` shares that tolerance, but its operand is still read (PHP + // consults `__isset` then `__get`), so it stays on the eager path with only + // the probe binding added. + let is_empty = php_symbol_key(builtin_name) == "empty"; + // An array-callback builtin types its callback's unannotated parameters + // from the array element/key inside `check_builtin`. Skip that argument + // here: the eager pass would otherwise check the closure body against the + // unhinted parameter fallback and reject valid PHP such as + // `array_filter($strings, fn($v) => strlen($v) > 5)`. + let contextual_callbacks = + crate::types::checker::builtins::contextual_callback_arg_positions( + builtin_name, + ); for (idx, arg) in expanded_args.iter().enumerate() { - if builtin_name.eq_ignore_ascii_case("preg_replace_callback") && idx == 1 { + if contextual_callbacks.contains(&idx) { continue; } if builtin_name.eq_ignore_ascii_case("preg_match") && idx == 2 { continue; } - // The user-sort comparator is type-checked by `check_builtin` - // with its parameters typed from the array element (so an - // unannotated object comparator type-checks). Skip the eager - // pass here, which would otherwise check the comparator body - // with default `Int` parameters and reject object access. - if idx == 1 - && (builtin_name.eq_ignore_ascii_case("usort") - || builtin_name.eq_ignore_ascii_case("uasort") - || builtin_name.eq_ignore_ascii_case("uksort")) - { + if is_empty { + let probe = null_probe::begin_null_probe_root(self, arg, env); + let effects = self.infer_type_with_assignment_effects(arg, env); + null_probe::end_null_probe_root(probe, env); + effects?; continue; } self.infer_type_with_assignment_effects(arg, env)?; diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index 0ab7a704a1..8e50fab9c7 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -490,3 +490,51 @@ mod tests { ); } } + +impl Checker { + + /// Rejects `++` / `--` on a `string` local whose storage cannot be boxed to `Mixed`. + /// + /// The operator can retype its target (`"9"++` is `int(10)`), which elephc implements by + /// giving the local boxed `Mixed` frame storage. Two storage shapes cannot take that + /// contract: a by-reference parameter aliases a caller slot whose declared `string` type + /// the callee must not change, and a `static` local's initializer writes its symbol with + /// the declared `string` representation before any boxing store runs. Both are rejected + /// here so the program gets a source-level diagnostic instead of a backend error or a + /// silently wrong value. + fn reject_unboxable_string_incdec( + &self, + name: &str, + span: Span, + ) -> Result<(), CompileError> { + let storage = if self.active_ref_params.contains(name) { + "a by-reference parameter" + } else if self.active_statics.contains(name) { + "a static local" + } else { + return Ok(()); + }; + Err(CompileError::new( + span, + &format!( + "Cannot increment/decrement ${} of type string: it is {}, and PHP's string \ + increment can change the value's type (\"9\"++ is int(10)), which that \ + storage cannot hold. Copy it into a plain local first.", + name, storage + ), + )) + } + + /// Records that `name` is a `string` local used as a `++` / `--` target in the + /// function-like scope currently being checked. + /// + /// EIR lowering reads this contract (through `CheckResult::string_incdec_locals`) and + /// gives the local boxed `Mixed` frame storage from its first store. Without it the + /// slot only widens at the increment, and every earlier or later `string`-typed read + /// of the same slot has to detach an owned copy out of the boxed cell — one leaked + /// heap block per executed read, unbounded inside a loop. + fn record_string_incdec_local(&mut self, name: &str) { + self.string_incdec_locals + .insert((self.current_loop_storage_scope.clone(), name.to_string())); + } +} diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index a6c0dae2f2..b715d0c03c 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -106,6 +106,11 @@ impl Checker { if matches!(obj_ty, PhpType::Mixed) { return Ok(PhpType::Mixed); } + // `isset($n->p)` / `$n->p ?? $d` reach through a null receiver in PHP and answer + // `false` / the default; only a probe context may do so. + if matches!(obj_ty, PhpType::Void) && self.null_probe_depth > 0 { + return Ok(PhpType::Void); + } Err(CompileError::new( expr.span, "Property access requires an object or typed pointer", @@ -375,12 +380,22 @@ impl Checker { /// Resolves the static receiver (named, `self::`, `static::`, `parent::`) /// to a class name, then looks up the declared static property type after /// validating visibility rules. + /// + /// A flow narrowing recorded for the same place (`self::$p === null` guards, + /// `self::$p = ` writes) wins over the declared type, the same way + /// `infer_property_access_type` consults its instance-property key. pub(crate) fn infer_static_property_access_type( &mut self, receiver: &StaticReceiver, property: &str, expr: &Expr, + env: &TypeEnv, ) -> Result { + if let Some(key) = self.narrowed_static_property_env_key(receiver, property, expr) { + if let Some(narrowed) = env.get(&key) { + return Ok(narrowed.clone()); + } + } let class_name = self.resolve_static_property_receiver(receiver, expr)?; let Some(class_info) = self.classes.get(&class_name) else { if self.eval_barrier_active && matches!(receiver, StaticReceiver::Named(_)) { diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 26ae6ba224..44cac68415 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -138,12 +138,13 @@ impl Checker { } else { effective_sig }; - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, env, &format!("Constructor '{}::__construct'", class_name), + class_name.as_str(), )?; for (i, arg) in normalized_args.iter().enumerate() { let arg_ty = self.infer_type(arg, env)?; @@ -255,12 +256,13 @@ impl Checker { &format!("Constructor '{}::__construct'", class_name), env, )?; - self.check_known_callable_call( + self.check_user_declared_call( &sig, &normalized_args, expr.span, env, &format!("Constructor '{}::__construct'", class_name), + class_name, )?; if class_name == "ReflectionParameter" { diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 888b97502f..78f8218676 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -279,12 +279,13 @@ impl Checker { &format!("Method {}::{}", interface_name, method), env, )?; - self.check_known_callable_call( + self.check_user_declared_call( &sig, &normalized_args, expr.span, env, &format!("Method {}::{}", interface_name, method), + interface_name, )?; let late_static_return = self.instance_method_late_static_return(interface_name, &method_key); match late_static_return { @@ -416,20 +417,22 @@ impl Checker { env, )?; if allow_by_ref_spread { - self.check_known_callable_call_allowing_by_ref_spread( + self.check_user_declared_call_allowing_by_ref_spread( &effective_sig, &normalized_args, expr.span, env, &format!("Method {}::{}", class_name, method), + class_name, )?; } else { - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, env, &format!("Method {}::{}", class_name, method), + class_name, )?; } } else if let Some(sig) = class_info.methods.get("__call") { @@ -446,20 +449,22 @@ impl Checker { env, )?; if allow_by_ref_spread { - self.check_known_callable_call_allowing_by_ref_spread( + self.check_user_declared_call_allowing_by_ref_spread( &effective_sig, &normalized_args, expr.span, env, &format!("Method {}::__call", class_name), + class_name, )?; } else { - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, env, &format!("Method {}::__call", class_name), + class_name, )?; } magic_return_ty = Some(effective_sig.return_type.clone()); @@ -550,6 +555,12 @@ impl Checker { && sig.variadic.is_some() && arg_types.len() > regular_param_count && !method_variadic_param_is_by_ref(sig) + // A declared element type on the variadic (`mixed ...$xs`, `int ...$xs`) is + // the contract, exactly like a declared regular parameter above: call-site + // arguments are validated against it and must never narrow it. Without this + // guard `mixed ...$xs` was rewritten to the widened argument type, and a + // later checker pass then rejected the very call that produced it. + && !declared_flags.get(regular_param_count).copied().unwrap_or(false) { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { @@ -896,20 +907,22 @@ impl Checker { env, )?; if allow_by_ref_spread { - self.check_known_callable_call_allowing_by_ref_spread( + self.check_user_declared_call_allowing_by_ref_spread( &effective_sig, &normalized_args, expr.span, env, &format!("Static method {}::{}", class_name, method), + class_name, )?; } else { - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, env, &format!("Static method {}::{}", class_name, method), + class_name, )?; } } else if parent_call || self_call { @@ -963,7 +976,7 @@ impl Checker { env, )?; if allow_by_ref_spread { - self.check_known_callable_call_allowing_by_ref_spread( + self.check_user_declared_call_allowing_by_ref_spread( &effective_sig, &normalized_args, expr.span, @@ -974,9 +987,10 @@ impl Checker { class_name, method ), + class_name, )?; } else { - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, @@ -987,6 +1001,7 @@ impl Checker { class_name, method ), + class_name, )?; } } else if class_info.methods.contains_key(&method_key) { @@ -1012,20 +1027,22 @@ impl Checker { env, )?; if allow_by_ref_spread { - self.check_known_callable_call_allowing_by_ref_spread( + self.check_user_declared_call_allowing_by_ref_spread( &effective_sig, &normalized_args, expr.span, env, &format!("Static method {}::__callStatic", class_name), + class_name, )?; } else { - self.check_known_callable_call( + self.check_user_declared_call( &effective_sig, &normalized_args, expr.span, env, &format!("Static method {}::__callStatic", class_name), + class_name, )?; } magic_return_ty = Some(effective_sig.return_type.clone()); @@ -1125,6 +1142,12 @@ impl Checker { && sig.variadic.is_some() && arg_types.len() > regular_param_count && !method_variadic_param_is_by_ref(sig) + // Same rule as the instance-method path: a declared variadic element type is + // a contract to validate against, not a slot to narrow from the call site. + && !static_declared_flags + .get(regular_param_count) + .copied() + .unwrap_or(false) { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index c11dcd5a63..e4596b1586 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -45,7 +45,21 @@ impl Checker { "Exponentiation requires numeric operands", )); } - Ok(PhpType::Float) + // PHP's `**` is int-preserving: `2 ** 3` is `int(8)`, not `float(8)`. + // It only becomes a float when an operand is already a float, when the + // exponent is negative, or when the integer result overflows `i64` — so + // an int/int power is `Mixed` for the same reason `+`/`-`/`*` are. + if uses_mixed_numeric_dispatch(<) || uses_mixed_numeric_dispatch(&rt) { + Ok(PhpType::Mixed) + } else if lt == PhpType::Float || rt == PhpType::Float { + Ok(PhpType::Float) + } else if let Some(literal_ty) = + checked_literal_int_arithmetic_type(op, left, right) + { + Ok(literal_ty) + } else { + Ok(PhpType::Mixed) + } } BinOp::Add => { if is_array_like_type(<) || is_array_like_type(&rt) { @@ -421,6 +435,9 @@ impl Checker { })?; if var_ty != PhpType::Callable { if matches!(var_ty.codegen_repr(), PhpType::Str) { + // The callee name is only known at runtime, but PHP rejects + // unpacking after named arguments while compiling the call. + self.require_no_spread_after_named_args(args, &format!("callable ${}", var))?; for arg in args { self.infer_type(arg, env)?; } @@ -507,6 +524,9 @@ impl Checker { &format!("callable ${}", var), ); } + // No signature is known for this callable, so the planner never runs; + // still apply PHP's syntactic unpack-after-named rule. + self.require_no_spread_after_named_args(args, &format!("callable ${}", var))?; for arg in args { self.infer_type(arg, env)?; } @@ -535,6 +555,13 @@ impl Checker { } let callee_ty = self.infer_type(callee, env)?; if matches!(callee_ty.codegen_repr(), PhpType::Str) { + // String callables resolve at runtime; PHP still rejects unpacking + // after named arguments while compiling the call expression. + let callee_desc = match &callee.kind { + ExprKind::Variable(var_name) => format!("callable ${}", var_name), + _ => "callable expression".to_string(), + }; + self.require_no_spread_after_named_args(args, &callee_desc)?; for arg in args { self.infer_type(arg, env)?; } @@ -1265,11 +1292,47 @@ fn checked_literal_int_arithmetic_type(op: &BinOp, left: &Expr, right: &Expr) -> BinOp::Add => lhs.checked_add(rhs).is_some(), BinOp::Sub => lhs.checked_sub(rhs).is_some(), BinOp::Mul => lhs.checked_mul(rhs).is_some(), + BinOp::Pow => int_pow_result_fits(lhs, rhs), _ => return None, }; Some(if fits { PhpType::Int } else { PhpType::Float }) } +/// Returns whether PHP's `int ** int` keeps an integer result for these literals. +/// +/// Mirrors `zend_pow_function_base` (and `crate::optimize::fold::ops::try_fold_int_pow`): +/// a negative exponent is always a double, `exp == 0` and `base == 0` answer immediately, +/// and otherwise the square-and-multiply loop reports the first `i64` multiplication that +/// would overflow — the exact point where PHP promotes the result to a double. +fn int_pow_result_fits(base: i64, exponent: i64) -> bool { + if exponent < 0 { + return false; + } + if exponent == 0 || base == 0 { + return true; + } + let (mut accumulated, mut factor, mut remaining) = (1i64, base, exponent); + while remaining >= 1 { + if remaining % 2 == 1 { + remaining -= 1; + match accumulated.checked_mul(factor) { + Some(product) => accumulated = product, + None => return false, + } + } else { + remaining /= 2; + match factor.checked_mul(factor) { + Some(product) => factor = product, + None => return false, + } + } + if remaining == 0 { + return true; + } + } + true +} + /// Returns `true` when an integer arithmetic expression cannot overflow. fn int_arithmetic_identity_is_always_int(op: &BinOp, left: &Expr, right: &Expr) -> bool { match op { diff --git a/src/types/checker/inference/syntactic.rs b/src/types/checker/inference/syntactic.rs index c974beae82..4b39f117e3 100644 --- a/src/types/checker/inference/syntactic.rs +++ b/src/types/checker/inference/syntactic.rs @@ -310,18 +310,34 @@ pub fn infer_expr_type_syntactic(expr: &Expr) -> PhpType { | "ucwords" | "str_pad" | "implode" | "sprintf" | "vsprintf" | "nl2br" | "wordwrap" | "md5" | "sha1" | "hash" | "substr_replace" | "addslashes" | "stripslashes" | "htmlspecialchars" | "htmlentities" | "html_entity_decode" | "urlencode" | "urldecode" - | "base64_encode" | "base64_decode" | "bin2hex" | "hex2bin" | "number_format" + | "base64_encode" | "bin2hex" | "hex2bin" | "number_format" | "date" | "json_encode" | "json_decode" | "json_last_error_msg" | "gettype" - | "str_word_count" | "chunk_split" => PhpType::Str, - "strpos" | "strrpos" | "array_search" | "grapheme_strrev" | "fileatime" + | "chunk_split" | "quotemeta" | "base_convert" + // `join` is `implode`'s alias, and dechex/decbin/decoct render integers as + // strings. Without these arms an array literal such as `[dechex($n)]` would take + // the `_ => PhpType::Int` fallback below, type the element `int`, and read the + // string result registers as an integer — `["a"]` came out as `[0]`. + | "join" | "dechex" | "decbin" | "decoct" => PhpType::Str, + "strpos" | "strrpos" | "stripos" | "strripos" + | "array_search" | "grapheme_strrev" | "fileatime" | "filectime" | "fileperms" | "fileowner" | "filegroup" | "fileinode" | "filetype" | "stat" | "lstat" | "fstat" | "fgetc" | "readfile" - | "readlink" | "stream_get_contents" | "stream_copy_to_stream" | "clamp" => { + | "readlink" | "stream_get_contents" | "stream_copy_to_stream" | "clamp" + // hexdec/bindec/octdec return `int|float`, whose shared codegen representation + // is `Mixed`; the boxed cell must not be read back as a raw integer. + | "hexdec" | "bindec" | "octdec" + // `base64_decode()` returns `string|false` because `$strict = true` rejects a + // character outside the Base64 alphabet with `false`. Its representation is the + // same boxed `Mixed` cell, so it must not be read back as a string pair. + | "base64_decode" => { PhpType::Mixed } "fopen" | "tmpfile" => PhpType::Union(vec![PhpType::stream_resource(), PhpType::False]), "strlen" | "ord" | "count" | "intval" | "abs" | "intdiv" | "printf" - | "rand" | "time" | "fpassthru" | "linkinfo" => PhpType::Int, + | "rand" | "time" | "fpassthru" | "linkinfo" + // Listed explicitly rather than left to the `_ => PhpType::Int` fallback, so a + // future change to that fallback cannot silently retype them. + | "substr_count" | "strncmp" | "strncasecmp" => PhpType::Int, "floatval" | "floor" | "ceil" | "round" | "sqrt" | "pow" | "fmod" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "atan2" | "sinh" | "cosh" | "tanh" | "log" | "log2" | "log10" | "exp" | "hypot" | "pi" | "deg2rad" | "rad2deg" => PhpType::Float, @@ -462,7 +478,20 @@ pub fn infer_expr_type_syntactic(expr: &Expr) -> PhpType { PhpType::Int } } - BinOp::Div | BinOp::Pow => PhpType::Float, + BinOp::Div => PhpType::Float, + BinOp::Pow => { + // PHP's `**` keeps an integer result when both operands are ints, the + // exponent is non-negative and the value fits; otherwise it is a float. + let lt = infer_expr_type_syntactic(left); + let rt = infer_expr_type_syntactic(right); + if lt == PhpType::Float || rt == PhpType::Float { + PhpType::Float + } else if let Some(ty) = checked_literal_int_arithmetic_type(op, left, right) { + ty + } else { + PhpType::Mixed + } + } BinOp::Eq | BinOp::NotEq | BinOp::Lt @@ -490,11 +519,47 @@ fn checked_literal_int_arithmetic_type(op: &BinOp, left: &Expr, right: &Expr) -> BinOp::Add => lhs.checked_add(rhs).is_some(), BinOp::Sub => lhs.checked_sub(rhs).is_some(), BinOp::Mul => lhs.checked_mul(rhs).is_some(), + BinOp::Pow => int_pow_result_fits(lhs, rhs), _ => return None, }; Some(if fits { PhpType::Int } else { PhpType::Float }) } +/// Returns whether PHP's `int ** int` keeps an integer result for these literals. +/// +/// Mirrors `zend_pow_function_base` (and `crate::optimize::fold::ops::try_fold_int_pow`): +/// a negative exponent is always a double, `exp == 0` and `base == 0` answer immediately, +/// and otherwise the square-and-multiply loop reports the first `i64` multiplication that +/// would overflow — the exact point where PHP promotes the result to a double. +fn int_pow_result_fits(base: i64, exponent: i64) -> bool { + if exponent < 0 { + return false; + } + if exponent == 0 || base == 0 { + return true; + } + let (mut accumulated, mut factor, mut remaining) = (1i64, base, exponent); + while remaining >= 1 { + if remaining % 2 == 1 { + remaining -= 1; + match accumulated.checked_mul(factor) { + Some(product) => accumulated = product, + None => return false, + } + } else { + remaining /= 2; + match factor.checked_mul(factor) { + Some(product) => factor = product, + None => return false, + } + } + if remaining == 0 { + return true; + } + } + true +} + /// Returns `true` when an integer arithmetic expression cannot overflow. fn int_arithmetic_identity_is_always_int(op: &BinOp, left: &Expr, right: &Expr) -> bool { match op { diff --git a/src/types/checker/method_pass.rs b/src/types/checker/method_pass.rs index ff848d21c8..cb3c43f125 100644 --- a/src/types/checker/method_pass.rs +++ b/src/types/checker/method_pass.rs @@ -247,6 +247,14 @@ impl Checker { /// it always throws/exits/loops). `Never` combined with a body that *does* contain /// return statements produces a compile error. Generic array hints are passed /// through as-is to preserve inference. + /// + /// A method body containing `yield` is a generator: calling it produces a + /// `Generator` object regardless of what the body's `return` statements say, so + /// generator detection short-circuits the whole inference/validation chain the + /// same way the free-function path in `functions::resolution::signature` does. + /// Without that short-circuit an unhinted generator method infers `void` (the + /// body has no value return) and a `: Generator` hint trips the + /// "must return a value on every path" coverage check. fn update_method_return_type( &mut self, class: &FlattenedClass, @@ -276,7 +284,20 @@ impl Checker { Some(widest) }; let inferred_return = raw_inferred.clone().unwrap_or(PhpType::Void); - let effective_return = if let Some(type_ann) = method.return_type.as_ref() { + let effective_return = if crate::types::checker::yield_validation::body_contains_yield( + &method.body, + ) { + match self.generator_method_return_type(class, method) { + Ok(generator_ty) => generator_ty, + Err(error) => { + pass_errors.extend(error.flatten()); + self.current_class = None; + self.current_method = None; + self.current_method_is_static = false; + return; + } + } + } else if let Some(type_ann) = method.return_type.as_ref() { match self.resolve_declared_return_type_hint( type_ann, method.span, @@ -368,6 +389,39 @@ impl Checker { ); } + /// Resolves the return type of a method whose body contains `yield`. + /// + /// The result is always `Generator`, because that is the object PHP hands back when + /// the generator method is called. A declared return hint is still resolved and + /// validated: hints that accept a `Generator` (`Generator`, `Traversable`, + /// `iterable`, `mixed`, …) pass through, anything else is reported as an + /// incompatible return type. Unlike the non-generator path there is no + /// return-coverage check — a generator body legitimately has no `return` at all. + fn generator_method_return_type( + &mut self, + class: &FlattenedClass, + method: &ClassMethod, + ) -> Result { + let generator_ty = PhpType::Object("Generator".to_string()); + if let Some(type_ann) = method.return_type.as_ref() { + let declared = self.resolve_declared_return_type_hint( + type_ann, + method.span, + &format!("Method '{}::{}'", class.name, method.name), + )?; + if !self.generator_return_type_accepts(&declared) { + self.require_compatible_return_type( + &declared, + &generator_ty, + true, + method.span, + &format!("Method '{}::{}' return type", class.name, method.name), + )?; + } + } + Ok(generator_ty) + } + /// Updates callable-return metadata for one checked method body. fn update_method_callable_return_metadata( &mut self, diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 3f587d181b..3acf16d4e8 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -26,6 +26,7 @@ mod functions; mod inference; mod loop_storage; mod method_pass; +pub(crate) mod null_probe; mod schema; mod stmt_check; mod type_compat; @@ -83,6 +84,14 @@ pub(crate) struct Checker { /// Tracks callable signatures inferred for user-function callable parameters, /// keyed by (function_name, param_name). pub callable_param_sigs: HashMap<(String, String), FunctionSig>, + /// Whether the statement currently being checked was written in a file that opened with + /// `declare(strict_types=1)`. + /// + /// PHP scopes the directive to the file containing the *call site*, so this is installed + /// from `Stmt::strict_types` by `check_stmt` and restored afterwards. It is `false` outside + /// any statement check, which keeps class/constant/default-value checking on PHP's coercive + /// rules — the behaviour elephc had before the directive was honoured. + pub strict_types: bool, /// Tracks which undeclared function parameters have already had their type /// adopted from a real call site, keyed by (function_name, param_index). The /// first such call adopts the actual argument type; later disagreeing calls @@ -167,6 +176,38 @@ pub(crate) struct Checker { /// Once set, unknown local reads are treated as dynamic `Mixed` values because /// eval fragments can create caller-scope variables at runtime. pub eval_barrier_active: bool, + /// Types recorded for `return ;` statements at the moment each one was checked, + /// keyed by the statement node's address in the AST plus its span. + /// + /// `collect_return_infos` runs *after* a body has been checked and re-infers every return + /// against the body's FINAL environment. Without this side channel a flow-sensitive fact + /// that only holds on part of the body — a property narrowing established halfway down — + /// would be applied to returns that execute before it, silently accepting a nullable + /// return. The address key is exact because both passes borrow the same immutable AST; the + /// span is carried alongside so a recycled address can never be mistaken for a hit. + pub flow_typed_returns: HashMap, + /// Whether the statements being checked belong to the top-level (global) scope rather than a + /// function, method, or closure body. `with_local_storage_context` clears it for every local + /// scope, so `null_probe` only records deferred roots for the scope whose environment + /// actually becomes `CheckResult::global_env`. + pub null_probe_scope_is_top_level: bool, + /// Never-declared variables named by a null probe (`isset`/`empty`/`unset`/`??`) at top level, + /// with the span to blame if the tolerance turns out to be unjustified. + /// + /// PHP answers these probes without an `Undefined variable` warning, but EIR lowering can only + /// represent the storage when the variable stays `null` for the whole scope: main's local + /// types come from `global_env`, so a name that is *also* assigned somewhere at top level ends + /// up with that assigned type and its slot is read before any store. `check_top_level_program` + /// therefore defers the decision to the end of the pass, where `global_env` is authoritative. + pub pending_null_probe_roots: Vec<(String, Span)>, + /// Nesting depth of null-probe operand inference (`isset`/`empty`/`unset` arguments and the + /// left operand of `??`). + /// + /// Inside a probe, reaching through a `null` base is legal PHP — `isset($n['k'])` and + /// `$n->p ?? $d` answer `false`/`$d` for a null `$n` instead of faulting — so index and + /// property access on `PhpType::Void` yield `Void` rather than a diagnostic. Outside a probe + /// those accesses keep their errors. + pub null_probe_depth: usize, /// Active break/continue target depth in the current function or closure body. pub break_continue_depth: usize, /// Stacks of break/continue depths at each enclosing `finally` block boundary, @@ -191,6 +232,14 @@ pub(crate) struct Checker { pub builtin_call_types: HashMap, /// Fixed-point storage contracts keyed by function-like scope and loop span. pub loop_storage_types: crate::types::LoopStorageTypes, + /// `(scope, local)` pairs for `string` locals used as a `++`/`--` target. + /// + /// PHP's string increment can change the value's type (`"9"++` is `int(10)`), so EIR + /// lowering must give those locals boxed `Mixed` frame storage from their FIRST store + /// instead of widening the slot at the increment. Recorded here because the checker + /// already visits every expression with a typed environment, so no second AST walk is + /// needed. See `crate::ir_lower::context::LoweringContext::boxed_incdec_storage_type`. + pub string_incdec_locals: HashSet<(String, String)>, } #[derive(Clone)] @@ -269,6 +318,7 @@ pub fn check_types( throw_access_sites: checker.throw_access_sites, builtin_call_types: checker.builtin_call_types, loop_storage_types: checker.loop_storage_types, + string_incdec_locals: checker.string_incdec_locals, }) } diff --git a/src/types/checker/null_probe.rs b/src/types/checker/null_probe.rs new file mode 100644 index 0000000000..f88800997f --- /dev/null +++ b/src/types/checker/null_probe.rs @@ -0,0 +1,218 @@ +//! Purpose: +//! Models PHP's "null probe" constructs — `isset()`, `empty()`, `unset()` and the left operand +//! of `??` / `??=` — which exist precisely to name storage that may never have been declared. +//! PHP answers all of them without an `Undefined variable` warning. +//! +//! Called from: +//! - `crate::types::checker::builtins::language_constructs` (`isset`/`empty`/`unset` operands) +//! - `crate::types::checker::inference::expr` and `::effects` (`??` left operand) +//! - `crate::types::checker::driver::top_level` (deferred validation of recorded roots) +//! +//! Key details: +//! - Only the *spine* of the access chain is covered. PHP still warns about an undefined index +//! expression (`isset($a[$b])` warns for `$b`, not for `$a`), so index and property-name +//! subexpressions keep the ordinary `Undefined variable` diagnostic. +//! - A tolerated root is typed `PhpType::Void` (elephc's `null`), which is exactly what PHP +//! reports for a never-declared variable. +//! - Acceptance is **deferred, not immediate**, for top-level code. EIR lowering derives main's +//! local types from `CheckResult::global_env`, so a probed name can only be lowered correctly +//! when it stays `null` for the whole scope: `global_env` must end the pass without a binding +//! for it, so the slot is typed `Void` and codegen answers from the type instead of reading +//! uninitialized storage. A name that is *also* assigned at top level +//! (`if (!isset($cfg)) { $cfg = 3; }`) would instead get that assigned type on a slot the probe +//! reads before any store, so `check_top_level_program` re-raises the original diagnostic for +//! it. See `Checker::pending_null_probe_roots`. + +use crate::errors::CompileError; +use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; +use crate::types::{PhpType, TypeEnv}; + +use super::Checker; + +/// Returns the never-declared root variable of a null-probe operand's access chain. +/// +/// Walks the chain spine through `$x[...]`, `$x->p` and `$x?->p` down to the base +/// `ExprKind::Variable`, and reports its name only when that name is absent from `env`. +/// Returns `None` for any other operand shape or when the root is already bound. +pub(crate) fn undefined_probe_root_variable<'a>(arg: &'a Expr, env: &TypeEnv) -> Option<&'a str> { + let mut current = arg; + loop { + match ¤t.kind { + ExprKind::Variable(name) => { + let name = name.as_str(); + return (!env.contains_key(name)).then_some(name); + } + ExprKind::ArrayAccess { array, .. } => current = array, + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => current = object, + _ => return None, + } + } +} + +/// Returns a probe environment for `arg`: a clone of `env` in which a never-declared chain root +/// is bound to `null`, or `None` when `env` already suffices. +/// +/// Callers infer the operand against the returned environment so that a probe of a never-declared +/// variable answers `null` instead of raising `Undefined variable`. The clone is deliberately not +/// propagated back to the caller's scope: PHP's probes do not create the variable +/// (`if (isset($z)) {} echo $z;` still warns). +pub(crate) fn null_probe_env(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Option { + let name = undefined_probe_root_variable(arg, env)?.to_string(); + record_pending_root(checker, &name, arg.span); + let mut probed = env.clone(); + probed.insert(name, PhpType::Void); + Some(probed) +} + +/// Installs a temporary `null` binding for `arg`'s never-declared chain root in `env`. +/// +/// Returns the bound name, which must be handed to [`end_null_probe_root`] once the operand has +/// been checked. Use this variant on the assignment-effect path, where the caller needs the +/// operand's writes to land in the real environment and therefore cannot infer against a +/// throwaway clone. +pub(crate) fn begin_null_probe_root( + checker: &mut Checker, + arg: &Expr, + env: &mut TypeEnv, +) -> Option { + let name = undefined_probe_root_variable(arg, env)?.to_string(); + record_pending_root(checker, &name, arg.span); + env.insert(name.clone(), PhpType::Void); + Some(name) +} + +/// Removes a temporary probe binding installed by [`begin_null_probe_root`]. +/// +/// The binding is kept when the probed operand itself gave the name a non-`null` type +/// (`empty($x = 5)` is legal PHP), so a real definition is never discarded. +pub(crate) fn end_null_probe_root(name: Option, env: &mut TypeEnv) { + let Some(name) = name else { return }; + if env.get(&name) == Some(&PhpType::Void) { + env.remove(&name); + } +} + +impl Checker { + /// Infers a null-probe operand: the operand of `isset`/`empty`/`unset` or the left side of + /// `??`. + /// + /// Raises [`Checker::null_probe_depth`] for the duration so index and property access on a + /// `null` base yield `null` instead of a diagnostic, matching PHP — `isset($n['k'])` and + /// `$n->p ?? $d` answer `false` / the default rather than faulting. Index and property-name + /// subexpressions are inferred inside the same context but are unaffected: only a `Void` + /// base changes behavior, so an undefined `$b` in `isset($a[$b])` still reports. + pub(crate) fn infer_null_probe_operand( + &mut self, + expr: &Expr, + env: &TypeEnv, + ) -> Result { + self.null_probe_depth += 1; + let result = self.infer_type(expr, env); + self.null_probe_depth -= 1; + result + } + + /// Infers a null-probe operand while propagating its assignment effects into `env`. + /// + /// Same contract as [`Checker::infer_null_probe_operand`], for the statement-effect walk. + pub(crate) fn infer_null_probe_operand_with_effects( + &mut self, + expr: &Expr, + env: &mut TypeEnv, + ) -> Result { + self.null_probe_depth += 1; + let result = self.infer_type_with_assignment_effects(expr, env); + self.null_probe_depth -= 1; + result + } +} + +/// Records a tolerated root for the end-of-pass check, but only for top-level scopes. +/// +/// Function, method, and closure bodies do not contribute their locals to `global_env`, so there +/// is nothing to validate the tolerance against there and nothing to seed for lowering. +fn record_pending_root(checker: &mut Checker, name: &str, span: Span) { + if !checker.null_probe_scope_is_top_level { + return; + } + checker + .pending_null_probe_roots + .push((name.to_string(), span)); +} + +/// Builds the diagnostic re-raised when a deferred probe root turns out to be assigned elsewhere +/// in the same scope, so the tolerance is not backed by a representable `null` slot. +pub(crate) fn unrepresentable_probe_root_error(name: &str, span: Span) -> CompileError { + CompileError::new(span, &format!("Undefined variable: ${}", name)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Builds a point-span expression for the probe-shape unit tests. + fn expr(kind: ExprKind) -> Expr { + Expr::new(kind, Span::new(1, 1)) + } + + /// A bare undefined variable is reported as the probe root. + #[test] + fn bare_undefined_variable_is_a_probe_root() { + let env = TypeEnv::new(); + let arg = expr(ExprKind::Variable("never".to_string())); + assert_eq!(undefined_probe_root_variable(&arg, &env), Some("never")); + } + + /// A variable that is already bound is not a probe root, so normal inference applies. + #[test] + fn defined_variable_is_not_a_probe_root() { + let mut env = TypeEnv::new(); + env.insert("known".to_string(), PhpType::Int); + let arg = expr(ExprKind::Variable("known".to_string())); + assert_eq!(undefined_probe_root_variable(&arg, &env), None); + } + + /// `$never['k']->p` resolves through the chain spine down to `$never`. + #[test] + fn chain_spine_resolves_to_the_base_variable() { + let env = TypeEnv::new(); + let arg = expr(ExprKind::PropertyAccess { + object: Box::new(expr(ExprKind::ArrayAccess { + array: Box::new(expr(ExprKind::Variable("never".to_string()))), + index: Box::new(expr(ExprKind::StringLiteral("k".to_string()))), + })), + property: "p".to_string(), + }); + assert_eq!(undefined_probe_root_variable(&arg, &env), Some("never")); + } + + /// Only the spine is reported: `isset($a[$b])` yields `$a`, leaving `$b` to ordinary + /// inference so it keeps PHP's undefined-variable diagnostic. + #[test] + fn index_subexpression_is_not_part_of_the_spine() { + let env = TypeEnv::new(); + let arg = expr(ExprKind::ArrayAccess { + array: Box::new(expr(ExprKind::Variable("a".to_string()))), + index: Box::new(expr(ExprKind::Variable("b".to_string()))), + }); + assert_eq!(undefined_probe_root_variable(&arg, &env), Some("a")); + } + + /// A non-lvalue operand shape yields no probe root. + #[test] + fn non_lvalue_operand_has_no_probe_root() { + let env = TypeEnv::new(); + let arg = expr(ExprKind::IntLiteral(1)); + assert_eq!(undefined_probe_root_variable(&arg, &env), None); + } + + /// The deferred diagnostic keeps the original wording, so a genuinely undefined variable is + /// reported identically whether or not a probe deferred the decision. + #[test] + fn deferred_diagnostic_matches_the_undefined_variable_wording() { + let error = unrepresentable_probe_root_error("cfg", Span::new(2, 5)); + assert_eq!(error.message, "Undefined variable: $cfg"); + } +} diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index 47b2e6c5b9..b57133acd4 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -233,6 +233,23 @@ pub(crate) fn validate_signature_compatibility( kind: &str, context: &str, ) -> Result<(), CompileError> { + // The hidden variadic that collects surplus positional arguments for + // `func_num_args()`/`func_get_args()`/`func_get_arg()` is a real ABI parameter, so an + // inherited signature that does not carry it cannot dispatch to a body that does. + // Report that directly instead of the generic parameter-count mismatch, which names a + // parameter the source never wrote. + if crate::func_args::sig_collects_surplus_args(child_sig) + != crate::func_args::sig_collects_surplus_args(parent_sig) + { + return Err(CompileError::new( + span, + &format!( + "func_num_args()/func_get_args()/func_get_arg() are not supported in {}::{} when {} {}: the inherited signature cannot be widened to collect surplus arguments", + owner_name, method_name, context, kind + ), + )); + } + if child_sig.params.len() != parent_sig.params.len() { return Err(CompileError::new( span, diff --git a/src/types/checker/stmt_check.rs b/src/types/checker/stmt_check.rs index ac7f061307..da8926f472 100644 --- a/src/types/checker/stmt_check.rs +++ b/src/types/checker/stmt_check.rs @@ -32,9 +32,17 @@ impl Checker { /// Returns an error for unresolved conditionals, namespace/use directives, /// includes, or invalid break/continue levels. pub fn check_stmt(&mut self, stmt: &Stmt, env: &mut TypeEnv) -> Result<(), CompileError> { - crate::strict_php::with_source_mode(stmt.source_mode, || { + // `declare(strict_types=1)` is scoped to the physical file the statement was written in, + // and the checker only ever sees the merged program, so the file's answer travels on the + // statement. Save/restore rather than assign: a strict file's `include` of a coercive one + // must not leave the includer's setting behind, and vice versa. + let outer_strict_types = self.strict_types; + self.strict_types = stmt.strict_types; + let result = crate::strict_php::with_source_mode(stmt.source_mode, || { self.check_stmt_in_current_source_mode(stmt, env) - }) + }); + self.strict_types = outer_strict_types; + result } /// Checks one statement after its physical source profile has been installed. @@ -112,7 +120,15 @@ impl Checker { StmtKind::FunctionDecl { .. } => Ok(()), StmtKind::Return(expr) => { if let Some(e) = expr { - self.infer_type_with_assignment_effects(e, env)?; + let returned = self.infer_type_with_assignment_effects(e, env)?; + // Record the type as observed HERE, in flow order, so the later + // flow-insensitive return-coverage pass does not apply a narrowing that + // only holds further down the body to this return. See + // `Checker::flow_typed_returns`. + self.flow_typed_returns.insert( + stmt as *const Stmt as usize, + (stmt.span, returned), + ); // `function &f() { return $obj->prop; }` returns a reference to the // property, so promote it to a reference property program-wide. if self.current_by_ref_return { diff --git a/src/types/checker/stmt_check/assignments.rs b/src/types/checker/stmt_check/assignments.rs index a525b355ec..525f71cd5d 100644 --- a/src/types/checker/stmt_check/assignments.rs +++ b/src/types/checker/stmt_check/assignments.rs @@ -235,14 +235,26 @@ impl Checker { result } - /// Invalidates synthetic property facts affected by a completed statement assignment. + /// Invalidates synthetic property facts affected by a completed statement assignment, then + /// re-establishes the fact for the storage the statement itself wrote. + /// /// Property writes can mutate an aliased object and therefore clear every fact; local - /// rebindings clear only facts rooted at the rebound local. - fn invalidate_property_narrowings_after_assignment(&self, stmt: &Stmt, env: &mut TypeEnv) { + /// rebindings clear only facts rooted at the rebound local. A plain + /// `$this->p = ` / `self::$p = ` write is the one case where the + /// post-write type of a place is known, so `record_property_assignment_narrowing` puts that + /// single fact back — this is what lets `if (self::$p === null) { self::$p = new S(); }` + /// leave `self::$p` non-null on both paths. + fn invalidate_property_narrowings_after_assignment(&mut self, stmt: &Stmt, env: &mut TypeEnv) { match &stmt.kind { - StmtKind::PropertyAssign { .. } - | StmtKind::PropertyArrayPush { .. } - | StmtKind::PropertyArrayAssign { .. } => Self::purge_property_narrowings(env), + StmtKind::PropertyAssign { .. } | StmtKind::StaticPropertyAssign { .. } => { + Self::purge_property_narrowings(env); + self.record_property_assignment_narrowing(stmt, env); + } + StmtKind::PropertyArrayPush { .. } | StmtKind::PropertyArrayAssign { .. } => { + Self::purge_property_narrowings(env) + } + StmtKind::StaticPropertyArrayPush { .. } + | StmtKind::StaticPropertyArrayAssign { .. } => Self::purge_property_narrowings(env), StmtKind::NestedArrayAssign { target, .. } if assignment_target_may_write_property(target) => { diff --git a/src/types/checker/stmt_check/control_flow.rs b/src/types/checker/stmt_check/control_flow.rs index 4b8b63953e..22f7b70676 100644 --- a/src/types/checker/stmt_check/control_flow.rs +++ b/src/types/checker/stmt_check/control_flow.rs @@ -311,6 +311,12 @@ impl Checker { // so each one can be restored after the construct. let mut saved_vars: Vec<(String, Option)> = Vec::new(); let mut applied_any_guard = false; + // Single-clause join state: the guarded key and the type it has where the + // then-branch falls out of the construct. `None` means "no usable fact" (the + // branch diverged, or a call inside it purged the narrowing). + let mut join_key: Option = None; + let mut then_exit_ty: Option = None; + let single_clause = clauses.len() == 1; for (cond, body) in &clauses { self.infer_type_with_assignment_effects(cond, env)?; @@ -330,6 +336,20 @@ impl Checker { errors.extend(error.flatten()); } } + // Join only when the then-branch WROTE the guarded place, i.e. when the + // fact at branch exit is no longer the guard's own `then` type. That is + // exactly the lazy-initialization shape; joining unconditionally would + // instead publish a guard's narrowing (e.g. `instanceof`) to the code + // after the `if`, where it does not hold. + let branch_exit = env.get(&guard.var); + if single_clause + && Self::narrowed_place_key_is_property(&guard.var) + && !self.body_cannot_fall_through(body) + && branch_exit.is_some_and(|ty| *ty != guard.then_ty) + { + join_key = Some(guard.var.clone()); + then_exit_ty = branch_exit.cloned(); + } restore_narrowed_var(env, &guard.var, &saved); // The fallthrough env for the rest of the chain (next elseif or else) @@ -346,12 +366,23 @@ impl Checker { } // Final else body (if present) is checked with the accumulated complement. + // `None` = the else path cannot reach the code after the `if`. `Some(None)` = it + // can, but the guarded fact was lost there. `Some(Some(ty))` = it can and the + // fact is `ty`. + let mut else_exit_ty: Option> = None; + let mut else_falls_through = else_body.is_none(); if let Some(body) = else_body { for s in body { if let Err(error) = self.check_stmt(s, env) { errors.extend(error.flatten()); } } + else_falls_through = !self.body_cannot_fall_through(body); + } + if let Some(key) = &join_key { + if else_falls_through { + else_exit_ty = Some(env.get(key).cloned()); + } } // Keep the accumulated complement for the statements after the `if` only when no @@ -363,10 +394,28 @@ impl Checker { && clauses .iter() .all(|(_, body)| self.body_cannot_fall_through(body)); + // A single guarded clause whose then-branch also falls through joins the two + // exit facts instead of discarding both. `if (X === null) { X = new S(); }` + // leaves `S` on the then path (the write recorded it) and `S` on the else path + // (the guard complement), so the union is `S` — the singleton pattern. + let joined = join_key.as_ref().and_then(|key| { + let then_ty = then_exit_ty.clone()?; + let joined = match &else_exit_ty { + None => then_ty, + Some(Some(else_ty)) => { + self.normalize_union_type(vec![then_ty, else_ty.clone()]) + } + Some(None) => return None, + }; + Some((key.clone(), joined)) + }); if !keep_complement_after_if { for (var, original) in &saved_vars { restore_narrowed_var(env, var, original); } + if let Some((key, joined)) = joined { + env.insert(key, joined); + } } if errors.is_empty() { diff --git a/src/types/checker/stmt_check/narrowing.rs b/src/types/checker/stmt_check/narrowing.rs index 6e5e09be15..a3146ea470 100644 --- a/src/types/checker/stmt_check/narrowing.rs +++ b/src/types/checker/stmt_check/narrowing.rs @@ -7,21 +7,33 @@ //! //! Key details: //! - Recognizes scalar, null, array, and callable `is_*($var)` predicates (and aliases), -//! `$var instanceof Class`, and strict null/false comparisons, optionally negated. Narrowing is -//! applied to each clause in an -//! if/elseif*/else chain (each subsequent clause, and the else, see the accumulated complement -//! from previous guards). For a chain with no else where *every* clause body cannot fall through -//! to the following statement — via `src/termination.rs`'s structural analysis +//! `$var instanceof Class`, `=== null` / `=== false` and their `!==` forms, and single-operand +//! `isset(...)`. `!==` and `isset` are self-negating guards, which combine with a leading `!` +//! the same way two negations cancel. Narrowing is applied to each clause in an if/elseif*/else +//! chain (each subsequent clause, and the else, see the accumulated complement from previous +//! guards). For a chain with no else where *every* clause body cannot fall through to the +//! following statement — via `src/termination.rs`'s structural analysis //! (return/throw/break/continue/exit/die, statically infinite loops, nested if/switch/try whose //! branches all terminate, or a terminal statement before unreachable code), extended //! recursively with checker-known `never` calls — the accumulated complement is applied to the //! statements after the entire if construct. -//! - Conservative: a concrete (non-union, non-mixed) type is left unchanged, and an empty narrowing -//! result falls back to the original type, so valid code is never narrowed away to `Never`. +//! - Guarded places are locals, simple instance properties (`$var->p`, `$this->p`) and simple +//! static properties (`self::$p`, `Cls::$p`); `static::$p` is excluded because late static +//! binding can select a different storage. Property places are keyed under a `\x01` sigil, so +//! `purge_property_narrowings` drops all of them after any call. +//! - A completed `$this->p = ` / `self::$p = ` write re-establishes the fact +//! for that place (as the DECLARED type minus null), and `control_flow` joins that branch-exit +//! fact with the guard complement. Together these make PHP's lazy-initialization idiom +//! (`if (self::$p === null) { self::$p = new S(); } return self::$p;`) type-check. +//! - Conservative: a concrete (non-union, non-mixed) type is left unchanged, an empty narrowing +//! result falls back to the original type, and guard detection never raises a diagnostic of its +//! own — an un-typeable receiver simply is not narrowed. use crate::errors::CompileError; use crate::names::{php_symbol_key, property_hook_get_method}; -use crate::parser::ast::{BinOp, Expr, ExprKind, InstanceOfTarget, Stmt}; +use crate::parser::ast::{ + BinOp, Expr, ExprKind, InstanceOfTarget, StaticReceiver, Stmt, StmtKind, +}; use crate::termination::{block_terminal_effect_with_divergence, TerminalEffect}; use crate::types::{PhpType, TypeEnv}; @@ -79,23 +91,40 @@ impl Checker { return Ok(None); }; let negated = prefix_negated ^ comparison_negated; - let Some(key) = Self::guard_env_key(receiver) else { + let Some(key) = self.guard_env_key(receiver) else { return Ok(None); }; - if self.property_guard_receiver_is_unstable(receiver, env)? { + // An un-typeable receiver is simply not narrowed. Guard detection must never be the + // thing that raises a diagnostic: the caller already inferred the condition through the + // normal path, which owns the real semantics — `isset($o->virtual)` is legal through + // `__isset` even though *reading* `$o->virtual` is not. + if self + .property_guard_receiver_is_unstable(receiver, env) + .unwrap_or(true) + { return Ok(None); } // A prior narrowing (or a variable binding) wins; otherwise a property receiver falls back // to its declared field type. An unbound plain variable stays un-narrowed. let current = match env.get(&key) { Some(ty) => ty.clone(), - None if matches!(receiver.kind, ExprKind::PropertyAccess { .. }) => { - self.infer_type(receiver, env)? + None + if matches!( + receiver.kind, + ExprKind::PropertyAccess { .. } | ExprKind::StaticPropertyAccess { .. } + ) => + { + match self.infer_type(receiver, env) { + Ok(ty) => ty, + Err(_) => return Ok(None), + } } None => return Ok(None), }; let matched = self.narrow_to(¤t, &target); let complement = self.narrow_complement(¤t, &target); + // `!` on the condition and a self-negating guard (`isset(...)`, `!== null`) each swap the + // branches, so two negations cancel out — `negated` is already that XOR. let (then_ty, else_ty) = if negated { (complement, matched) } else { @@ -116,18 +145,117 @@ impl Checker { } } + /// Synthetic `TypeEnv` key for a narrowed static property access (`self::$p`, `Cls::$p`). + /// + /// The receiver is resolved to its declaring class first, so `self::$p` and `Cls::$p` inside + /// `Cls` share one fact. `static::$p` is deliberately not keyed: late static binding can + /// select a subclass that redeclares the property, so the storage a guard observed is not + /// necessarily the storage a later read reaches. The key shares the `\x01` sigil with + /// instance-property keys, so `purge_property_narrowings` drops both after any call. + pub(crate) fn narrowed_static_property_env_key( + &self, + receiver: &StaticReceiver, + property: &str, + expr: &Expr, + ) -> Option { + if matches!(receiver, StaticReceiver::Static) { + return None; + } + let class_name = self.resolve_static_property_receiver(receiver, expr).ok()?; + Some(format!("\u{1}sprop\u{1}{class_name}::${property}")) + } + + /// Returns whether a narrowing key names a property place rather than a plain local. + /// + /// Synthetic property keys carry the `\x01` sigil, which no PHP variable name can contain. + /// Only these places can be re-established by a write inside a guarded branch, so the + /// post-`if` join in `control_flow` is limited to them. + pub(crate) fn narrowed_place_key_is_property(key: &str) -> bool { + key.starts_with('\u{1}') + } + /// `TypeEnv` key for a guard receiver: a variable's name, or the synthetic property key for a - /// simple property access. `None` for receivers narrowing can't key (complex chains). - fn guard_env_key(receiver: &Expr) -> Option { + /// simple instance/static property access. `None` for receivers narrowing can't key + /// (complex chains, `static::$p`). + fn guard_env_key(&self, receiver: &Expr) -> Option { match &receiver.kind { ExprKind::Variable(var) => Some(var.clone()), ExprKind::PropertyAccess { object, property } => { Self::narrowed_property_env_key(object, property) } + ExprKind::StaticPropertyAccess { + receiver: static_receiver, + property, + } => self.narrowed_static_property_env_key(static_receiver, property, receiver), _ => None, } } + /// Records the flow fact produced by a completed property or static-property write. + /// + /// Runs after `purge_property_narrowings` has dropped every fact the write (or the calls + /// inside its right-hand side) could have invalidated, so this only ever re-establishes a + /// fact for the exact storage that was just written. The recorded type is the property's + /// DECLARED type minus `null`, never the assigned expression's type: a declared property + /// coerces what it stores (`public ?int $x; $x = 1.0;` reads back as `int`), so narrowing + /// to "declared, definitely not null" is the strongest statement that stays sound. + /// + /// Nothing is recorded when the assigned value may itself be null, when the receiver is not + /// a simple keyable place, or when a read could run user code (`__get` / a `get` hook). + pub(crate) fn record_property_assignment_narrowing(&mut self, stmt: &Stmt, env: &mut TypeEnv) { + let (place, value) = match &stmt.kind { + StmtKind::PropertyAssign { + object, + property, + value, + } => ( + Expr::new( + ExprKind::PropertyAccess { + object: object.clone(), + property: property.clone(), + }, + stmt.span, + ), + value, + ), + StmtKind::StaticPropertyAssign { + receiver, + property, + value, + } => ( + Expr::new( + ExprKind::StaticPropertyAccess { + receiver: receiver.clone(), + property: property.clone(), + }, + stmt.span, + ), + value, + ), + _ => return, + }; + let Some(key) = self.guard_env_key(&place) else { + return; + }; + if self + .property_guard_receiver_is_unstable(&place, env) + .unwrap_or(true) + { + return; + } + let Ok(assigned) = self.infer_type(value, env) else { + return; + }; + if !type_is_definitely_non_null(&assigned) { + return; + } + let Ok(declared) = self.infer_type(&place, env) else { + return; + }; + let non_null = self.narrow_complement(&declared, &GuardTarget::Exact(PhpType::Void)); + env.insert(key, non_null); + } + /// Drops every synthetic property narrowing from the environment. Called after effects that /// may write a property (property assignments, any call — a callee can mutate the object), /// and at loop-body entry (a later iteration may observe an earlier iteration's write), so a @@ -247,11 +375,35 @@ impl Checker { } } -/// Extracts the guarded receiver, target, and comparison negation from a guard expression. +/// Returns whether an expression shape can be the keyed receiver of a type guard. +/// +/// Variables and simple instance/static property accesses are the places `guard_env_key` +/// can name; everything else is rejected here so a comparison against a complex chain is not +/// mistaken for a guard. +fn is_guard_receiver_shape(kind: &ExprKind) -> bool { + matches!( + kind, + ExprKind::Variable(_) + | ExprKind::PropertyAccess { .. } + | ExprKind::StaticPropertyAccess { .. } + ) +} + +/// Extracts the guarded receiver, the target, and whether the guard is self-negating from a +/// (syntactically non-negated) guard expression. +/// +/// Recognizes the scalar `is_*` predicates, `is_null`, `is_array`, `is_callable`, +/// `instanceof `, `=== false` / `=== null`, their `!==` counterparts, and single-operand +/// `isset()`. The third tuple element is `true` for guards that are true when the target does +/// NOT match (`isset`, `!==`), so `guard_narrowing` can combine it with a leading `!`. The +/// receiver may be any expression here — `guard_env_key` decides which receivers narrowing can +/// actually key. fn guard_receiver_and_target(cond: &Expr) -> Option<(&Expr, GuardTarget, bool)> { match &cond.kind { ExprKind::FunctionCall { name, args } if args.len() == 1 => { - let target = match name.as_str().to_ascii_lowercase().as_str() { + // `php_symbol_key` rather than a plain lowercase: it also folds the leading `\` and + // the namespace qualification, so `\is_int($x)` and `Ns\is_int($x)` narrow too. + let target = match php_symbol_key(name.trim_start_matches('\\')).as_str() { "is_int" | "is_integer" | "is_long" => GuardTarget::Exact(PhpType::Int), "is_float" | "is_double" | "is_real" => GuardTarget::Exact(PhpType::Float), "is_string" => GuardTarget::Exact(PhpType::Str), @@ -262,6 +414,12 @@ fn guard_receiver_and_target(cond: &Expr) -> Option<(&Expr, GuardTarget, bool)> "is_null" => GuardTarget::Exact(PhpType::Void), "is_callable" => GuardTarget::Exact(PhpType::Callable), "is_array" => GuardTarget::AnyArray, + // `isset($x)` is the exact negation of `$x === null` for a keyable place: true + // exactly when the storage holds a non-null value. This is what makes + // `if (!isset(self::$inst)) { self::$inst = new S(); }` narrow. + "isset" if is_guard_receiver_shape(&args[0].kind) => { + return Some((&args[0], GuardTarget::Exact(PhpType::Void), true)) + } _ => return None, }; Some((&args[0], target, false)) @@ -280,31 +438,30 @@ fn guard_receiver_and_target(cond: &Expr) -> Option<(&Expr, GuardTarget, bool)> // then-branch; the else-branch strips only that member (e.g. int|false → int) while a full // `bool` member remains. Enables the common // `if ($x === false) { throw; } return $x;` guard (ward-http StreamGuards::requireInt etc.). - ExprKind::BinaryOp { left, op, right } - if matches!(op, BinOp::StrictEq | BinOp::StrictNotEq) => - { - let (receiver, lit) = match (&left.kind, &right.kind) { - (ExprKind::Variable(_) | ExprKind::PropertyAccess { .. }, _) => { - (left.as_ref(), &right.kind) - } - (_, ExprKind::Variable(_) | ExprKind::PropertyAccess { .. }) => { - (right.as_ref(), &left.kind) - } - _ => return None, + // `!==` is the same guard with the branches swapped. + ExprKind::BinaryOp { + left, + op: op @ (BinOp::StrictEq | BinOp::StrictNotEq), + right, + } => { + let negates = matches!(op, BinOp::StrictNotEq); + // `is_guard_receiver_shape` rather than an inline `Variable | PropertyAccess` + // match: it also accepts a static property, which is what lets the singleton + // shape `if (self::$inst === null) { self::$inst = new S(); }` narrow. + let (receiver, lit) = if is_guard_receiver_shape(&left.kind) { + (left.as_ref(), &right.kind) + } else if is_guard_receiver_shape(&right.kind) { + (right.as_ref(), &left.kind) + } else { + return None; }; match lit { - ExprKind::BoolLiteral(false) => Some(( - receiver, - GuardTarget::Exact(PhpType::False), - matches!(op, BinOp::StrictNotEq), - )), + ExprKind::BoolLiteral(false) => { + Some((receiver, GuardTarget::Exact(PhpType::False), negates)) + } // `$x === null`: strip the null-ish member (elephc models a `?T` value's null as // Void), e.g. `?self` / self|null → self after `if ($x === null) { throw; }`. - ExprKind::Null => Some(( - receiver, - GuardTarget::Exact(PhpType::Void), - matches!(op, BinOp::StrictNotEq), - )), + ExprKind::Null => Some((receiver, GuardTarget::Exact(PhpType::Void), negates)), _ => None, } } @@ -312,7 +469,22 @@ fn guard_receiver_and_target(cond: &Expr) -> Option<(&Expr, GuardTarget, bool)> } } -/// Returns whether a union member matches the exact or array-family guard target. +/// Returns whether a type can never hold `null` on any path. +/// +/// `Mixed` and `Never` are treated as possibly-null because neither carries enough information +/// to prove otherwise; a union is non-null only when every member is. +fn type_is_definitely_non_null(ty: &PhpType) -> bool { + match ty { + PhpType::Void | PhpType::Never | PhpType::Mixed => false, + PhpType::Union(members) => members.iter().all(type_is_definitely_non_null), + _ => true, + } +} + +/// Returns true when a union member is compatible with a guard target, used to keep (then) or drop +/// (else) members. Exact targets require a matching variant; an `Object` target matches an object +/// member with the same class name (inheritance-aware narrowing is left for the future), and +/// `AnyArray` matches either array shape. fn guard_matches(member: &PhpType, target: &GuardTarget) -> bool { match target { GuardTarget::AnyArray => matches!(member, PhpType::Array(_) | PhpType::AssocArray { .. }), diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index 922603ee7c..bcceb4bd81 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -402,6 +402,7 @@ impl Checker { let saved_eval_barrier_active = self.eval_barrier_active; let saved_break_continue_depth = self.break_continue_depth; let saved_finally_break_continue_bases = self.finally_break_continue_bases.clone(); + let saved_null_probe_scope_is_top_level = self.null_probe_scope_is_top_level; self.active_ref_params = ref_param_names.into_iter().collect(); self.active_globals.clear(); @@ -410,6 +411,9 @@ impl Checker { self.eval_barrier_active = false; self.break_continue_depth = 0; self.finally_break_continue_bases.clear(); + // A function/method/closure body is not the scope whose environment becomes + // `global_env`, so null-probe roots found here must not be deferred against it. + self.null_probe_scope_is_top_level = false; let result = f(self); @@ -420,6 +424,7 @@ impl Checker { self.eval_barrier_active = saved_eval_barrier_active; self.break_continue_depth = saved_break_continue_depth; self.finally_break_continue_bases = saved_finally_break_continue_bases; + self.null_probe_scope_is_top_level = saved_null_probe_scope_is_top_level; result } diff --git a/src/types/math_constants.rs b/src/types/math_constants.rs new file mode 100644 index 0000000000..79adb3bf1f --- /dev/null +++ b/src/types/math_constants.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Defines PHP math integer constants exposed by elephc. +//! Keeps `round()`'s rounding-mode constants in one source of truth for type checking and codegen. +//! +//! Called from: +//! - `crate::types::checker` when registering predefined constants. +//! - `crate::codegen_support::prescan` when materializing constant literal values. +//! - `crate::name_resolver::names` when deciding which names bypass symbol-table resolution. +//! +//! Key details: +//! - Values must match php-src's `PHP_ROUND_HALF_*` exactly: `round()` validates its `$mode` +//! against this contiguous `1..=4` range and raises `ValueError` for anything else, so a +//! mismatch here would turn a valid PHP call into a runtime exception (or, worse, silently +//! pick the wrong tie-breaking rule). + +/// Tuple of `(name, value)` pairs for PHP math integer constants. +/// +/// `round()` uses these constants to select how exact `.5` ties are broken. +pub(crate) const MATH_INT_CONSTANTS: &[(&str, i64)] = &[ + ("PHP_ROUND_HALF_UP", 1), + ("PHP_ROUND_HALF_DOWN", 2), + ("PHP_ROUND_HALF_EVEN", 3), + ("PHP_ROUND_HALF_ODD", 4), +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies the rounding-mode constants carry php-src's exact values. + /// + /// `round()`'s omitted-`$mode` default and its `ValueError` range check both assume + /// `PHP_ROUND_HALF_UP == 1` and a contiguous `1..=4` range. + #[test] + fn round_modes_match_php() { + assert_eq!( + MATH_INT_CONSTANTS, + &[ + ("PHP_ROUND_HALF_UP", 1), + ("PHP_ROUND_HALF_DOWN", 2), + ("PHP_ROUND_HALF_EVEN", 3), + ("PHP_ROUND_HALF_ODD", 4), + ] + ); + } + + /// Asserts no duplicate names exist in `MATH_INT_CONSTANTS`. + #[test] + fn no_duplicate_constant_names() { + let mut names: Vec<&str> = MATH_INT_CONSTANTS.iter().map(|(n, _)| *n).collect(); + names.sort_unstable(); + let len_before = names.len(); + names.dedup(); + assert_eq!(names.len(), len_before, "duplicate math constant name"); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 2f6daae582..1339f68943 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -32,6 +32,10 @@ pub(crate) mod error_constants; mod ffi; /// JSON literal constant type inference. pub(crate) mod json_constants; +/// PHP math integer constants (`PHP_ROUND_HALF_*` rounding modes). +pub(crate) mod math_constants; +/// PHP parameter-binding rules: coercive scalar binding and callable-name strings. +pub(crate) mod param_binding; /// PHP type model and type environment for tracking variable types. mod model; /// Preg/PCRE flag constants shared by checker and codegen. @@ -47,6 +51,7 @@ pub(crate) mod session_constants; /// Function signature representation and builtin signature helpers. mod signatures; pub(crate) mod stream_constants; +pub(crate) mod string_constants; /// Type checker diagnostics and warnings. mod warnings; diff --git a/src/types/param_binding.rs b/src/types/param_binding.rs new file mode 100644 index 0000000000..4850b6f897 --- /dev/null +++ b/src/types/param_binding.rs @@ -0,0 +1,634 @@ +//! Purpose: +//! Owns the PHP parameter-binding rules that let a declared parameter accept an argument +//! of a different type: coercive scalar binding (`string $s` accepting `42`), callable-name +//! strings (`callable $f` accepting `"strtoupper"`), and the `declare(strict_types=1)` mode +//! that switches every one of those conversions off. +//! +//! Called from: +//! - `crate::types::checker::functions::resolution` (acceptance + diagnostics) +//! - `crate::ir_lower::expr` (the matching argument rewrite) +//! +//! Key details: +//! - `strict_types` is decided by the file the *call site* is written in, not the file the +//! callee is declared in, so the rejection below is driven by the checker's current statement +//! (`crate::parser::ast::Stmt::strict_types`) and never by the callee's signature. +//! - Under `strict_types=1` the checker rejects the call outright, so EIR lowering never reaches +//! the matching rewrite for a strict-mode violation. The rewrites stay coercive-only. +//! - The checker and EIR lowering MUST agree: every binding the checker accepts here is +//! rewritten by lowering through `rewrite_param_bound_arg`, and nothing else is. A binding +//! accepted without a matching rewrite would pass raw storage into a differently typed +//! parameter slot. +//! - Coercion is expressed as the equivalent explicit PHP cast (`(string)`, `(bool)`) or as a +//! replacement literal, so the runtime result is whatever elephc already produces for that +//! cast — no separate conversion semantics to keep in sync. +//! - Only *total* coercions (every value of the source type converts, with no PHP diagnostic) +//! run on runtime values. Partial coercions (`int $i` from a float or string) are limited to +//! compile-time constants, because PHP signals their failure modes with a runtime +//! `Deprecated:` notice or a `TypeError` and elephc has neither channel at a call boundary. + +use crate::parser::ast::{CallableTarget, CastType, Expr, ExprKind, StaticReceiver}; +use crate::names::Name; +use crate::types::PhpType; + +/// How a declared parameter binds an argument whose type does not already match. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum ParamBinding { + /// PHP binds the argument unchanged; nothing to do. + Identity, + /// PHP coerces the argument exactly, with no notice and no failure mode. The argument is + /// replaced by the equivalent explicit cast. + Cast(CastType), + /// PHP coerces a compile-time constant exactly. The argument is replaced by this literal. + Const(ExprKind), + /// The argument is a compile-time callable-name string bound to a `callable` parameter. + /// The argument is replaced by the equivalent first-class callable expression. + Callable(CallableTarget), + /// PHP binds the argument but emits a runtime `Deprecated:` notice first. elephc has no + /// runtime deprecation channel, so this is reported at compile time instead. + Deprecated(String), + /// PHP throws a `TypeError` when the call runs. + TypeError(String), + /// PHP would coerce, but only a runtime check can tell whether the conversion succeeds, + /// and elephc cannot raise PHP's runtime notice/`TypeError` at a parameter boundary. + NeedsRuntimeCheck(String), + /// No PHP parameter-binding rule applies; the caller keeps its existing diagnostic. + Rejected, +} + +/// Classifies how `expected` binds `arg` (whose inferred type is `actual`) under PHP's +/// default coercive parameter binding. +/// +/// A `ParamBinding::Callable` result only says the string is *syntactically* a callable name; +/// whether it resolves to something invocable is decided by the caller against its own symbol +/// tables (the checker rejects an unresolvable name, so lowering never sees one). +/// +/// Returns `ParamBinding::Rejected` whenever no PHP rule applies, leaving the caller's +/// existing type-mismatch diagnostic in charge. +pub(crate) fn classify_param_binding( + expected: &PhpType, + actual: &PhpType, + arg: &Expr, +) -> ParamBinding { + if expected == actual { + return ParamBinding::Identity; + } + if *expected == PhpType::Callable { + return classify_callable_string_binding(actual, arg); + } + match (expected.codegen_repr(), actual.codegen_repr()) { + // `string $s` accepts every other scalar. `(string)` is total for int, float and + // bool, produces no PHP notice, and elephc's cast already matches PHP byte for byte + // (including `(string)false === ""` and float precision). + (PhpType::Str, PhpType::Int | PhpType::Float | PhpType::Bool) => { + ParamBinding::Cast(CastType::String) + } + // `bool $b` accepts every other scalar; `(bool)` is total and notice-free. + (PhpType::Bool, PhpType::Float | PhpType::Str) => ParamBinding::Cast(CastType::Bool), + // `int $i` / `float $f` from a float or string is partial: PHP emits `Deprecated:` on a + // lossy conversion and throws `TypeError` on a non-numeric string, NaN, INF or an + // out-of-range float. Only a compile-time constant can be decided here. + (PhpType::Int, PhpType::Float | PhpType::Str) + | (PhpType::Float, PhpType::Str) => classify_numeric_binding(expected, arg), + _ => ParamBinding::Rejected, + } +} + +/// The four PHP scalar type declarations `declare(strict_types=1)` compares by identity. +/// +/// `false` collapses into `Bool` because it is a subtype of `bool`, not a separate scalar for +/// binding purposes. Every non-scalar declaration — objects, arrays, `iterable`, `callable`, +/// `mixed`, unions, elephc's `Mixed`/`Pointer`/`Resource` — is deliberately absent: strict mode +/// changes nothing about how those bind, so they must keep their existing acceptance rules. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StrictScalar { + Int, + Float, + Str, + Bool, +} + +impl StrictScalar { + /// Returns the PHP spelling used in a `TypeError` message and in a cast suggestion. + fn php_name(self) -> &'static str { + match self { + StrictScalar::Int => "int", + StrictScalar::Float => "float", + StrictScalar::Str => "string", + StrictScalar::Bool => "bool", + } + } +} + +/// Classifies a declared type as one of PHP's scalar type declarations, or `None` when strict +/// mode has nothing to say about it. +/// +/// The *declared* type is inspected rather than `codegen_repr()`, which would flatten a union to +/// `Mixed` and a resource to `Int` and so invent scalar identities strict mode must not judge. +fn strict_scalar_kind(ty: &PhpType) -> Option { + match ty { + PhpType::Int => Some(StrictScalar::Int), + PhpType::Float => Some(StrictScalar::Float), + PhpType::Str => Some(StrictScalar::Str), + PhpType::Bool | PhpType::False => Some(StrictScalar::Bool), + _ => None, + } +} + +/// Reports why `declare(strict_types=1)` forbids binding `actual` to a declared `expected` +/// parameter, or `None` when PHP performs the binding in strict mode too. +/// +/// PHP keeps exactly one implicit conversion under the directive — widening `int` to a declared +/// `float` — and throws `TypeError` for every other scalar pair, including the `bool`→`int`, +/// `int`→`string` and numeric-string→`int` conversions its coercive mode performs silently. +/// Verified against PHP 8.4.20 for the full 4x4 scalar matrix. +/// +/// Returns `None` for any pair involving a non-scalar declared type: strict mode does not change +/// how those bind, so the caller's normal compatibility rules stay in charge. +pub(crate) fn strict_param_binding_rejection( + expected: &PhpType, + actual: &PhpType, +) -> Option { + let expected_kind = strict_scalar_kind(expected)?; + let actual_kind = strict_scalar_kind(actual)?; + if expected_kind == actual_kind { + return None; + } + if expected_kind == StrictScalar::Float && actual_kind == StrictScalar::Int { + return None; + } + Some(format!( + "`declare(strict_types=1)` is active in this file, so PHP performs no conversion here \ + and throws `TypeError: ... must be of type {}, {} given`; add an explicit `({})` cast \ + at the call site", + expected_kind.php_name(), + actual_kind.php_name(), + expected_kind.php_name() + )) +} + +/// Classifies a string argument bound to a declared `callable` parameter. +/// +/// PHP accepts `"function"` and `"Class::method"` strings as callables and resolves them when +/// the call runs. elephc resolves callables statically, so only a compile-time-known string is +/// bindable; anything else gets a named diagnostic instead of storage that cannot be invoked. +fn classify_callable_string_binding(actual: &PhpType, arg: &Expr) -> ParamBinding { + if actual.codegen_repr() != PhpType::Str { + return ParamBinding::Rejected; + } + let ExprKind::StringLiteral(name) = &arg.kind else { + return ParamBinding::NeedsRuntimeCheck( + "a callable string must be a compile-time constant here, because elephc resolves \ + callables statically; pass a first-class callable (`strtoupper(...)`), a closure, \ + or a literal function name" + .to_string(), + ); + }; + match callable_target_from_name(name) { + Some(target) => ParamBinding::Callable(target), + None => ParamBinding::TypeError(format!( + "\"{}\" is not a valid callable name", + name.escape_debug() + )), + } +} + +/// Parses a PHP callable string into the equivalent first-class callable target. +/// +/// `"Class::method"` becomes a static-method target and everything else a plain function +/// target, matching the split `crate::ir_lower` already performs for `call_user_func` +/// string callbacks. A leading `\` is dropped because a callable string is always +/// fully qualified in PHP. Returns `None` for a string that cannot name anything. +pub(crate) fn callable_target_from_name(name: &str) -> Option { + let name = name.trim_start_matches('\\'); + if name.is_empty() { + return None; + } + if let Some((class_name, method)) = name.rsplit_once("::") { + let class_name = class_name.trim_start_matches('\\'); + if class_name.is_empty() || method.is_empty() || method.contains("::") { + return None; + } + return Some(CallableTarget::StaticMethod { + receiver: StaticReceiver::Named(Name::unqualified(class_name)), + method: method.to_string(), + }); + } + Some(CallableTarget::Function(Name::unqualified(name))) +} + +/// Classifies a float or string argument bound to an `int`/`float` parameter. +/// +/// Only literals are decided: PHP's failure modes for this direction are a runtime +/// `Deprecated:` notice (lossy conversion) and a runtime `TypeError` (non-numeric string, NaN, +/// INF, out-of-range float), neither of which elephc can raise at a parameter boundary. +fn classify_numeric_binding(expected: &PhpType, arg: &Expr) -> ParamBinding { + match (&expected.codegen_repr(), &arg.kind) { + (PhpType::Int, ExprKind::FloatLiteral(value)) => const_float_to_int(*value), + (PhpType::Int, ExprKind::StringLiteral(text)) => const_string_to_int(text), + (PhpType::Float, ExprKind::StringLiteral(text)) => const_string_to_float(text), + _ => ParamBinding::NeedsRuntimeCheck( + "PHP coerces this at run time and signals failure with a `Deprecated:` notice or a \ + `TypeError`, which elephc cannot raise at a parameter boundary; add an explicit \ + cast at the call site" + .to_string(), + ), + } +} + +/// Applies PHP's float-to-int parameter coercion to a literal. +/// +/// An integral, in-range float binds silently; a fractional one binds after PHP's +/// `Implicit conversion ... loses precision` deprecation; NaN, INF and out-of-range values +/// throw `TypeError`. +fn const_float_to_int(value: f64) -> ParamBinding { + if !value.is_finite() || value < -(2f64.powi(63)) || value >= 2f64.powi(63) { + return ParamBinding::TypeError(format!( + "PHP throws `TypeError` for the float {} at an `int` parameter", + php_float_text(value) + )); + } + if value.fract() != 0.0 { + return ParamBinding::Deprecated(format!( + "PHP emits `Deprecated: Implicit conversion from float {} to int loses precision` \ + and passes {}", + php_float_text(value), + value.trunc() as i64 + )); + } + ParamBinding::Const(ExprKind::IntLiteral(value as i64)) +} + +/// Applies PHP's string-to-int parameter coercion to a literal. +/// +/// A fully numeric integral string binds silently, a fully numeric fractional string binds +/// after PHP's float-string deprecation, and everything else — including a leading-numeric +/// string such as `"42abc"` — throws `TypeError`. +fn const_string_to_int(text: &str) -> ParamBinding { + let Some(scan) = scan_php_numeric_string(text) else { + return ParamBinding::TypeError(format!( + "PHP throws `TypeError` for the non-numeric string \"{}\" at an `int` parameter", + text.escape_debug() + )); + }; + if !scan.is_float { + return match scan.text.parse::() { + Ok(value) => ParamBinding::Const(ExprKind::IntLiteral(value)), + // An integral spelling that overflows `i64` is a float to PHP, so it reaches the + // same out-of-range `TypeError` as an oversized float literal. + Err(_) => ParamBinding::TypeError(format!( + "PHP throws `TypeError` for the out-of-range numeric string \"{}\" at an `int` \ + parameter", + text.escape_debug() + )), + }; + } + let Ok(value) = scan.text.parse::() else { + return ParamBinding::TypeError(format!( + "PHP throws `TypeError` for the numeric string \"{}\" at an `int` parameter", + text.escape_debug() + )); + }; + if value.fract() == 0.0 && value.is_finite() && value.abs() < 2f64.powi(63) { + return ParamBinding::Const(ExprKind::IntLiteral(value as i64)); + } + ParamBinding::Deprecated(format!( + "PHP emits `Deprecated: Implicit conversion from float-string \"{}\" to int loses \ + precision`", + text.escape_debug() + )) +} + +/// Applies PHP's string-to-float parameter coercion to a literal. +/// +/// A fully numeric string binds silently; every other string throws `TypeError`. +fn const_string_to_float(text: &str) -> ParamBinding { + match scan_php_numeric_string(text).and_then(|scan| scan.text.parse::().ok()) { + Some(value) => ParamBinding::Const(ExprKind::FloatLiteral(value)), + None => ParamBinding::TypeError(format!( + "PHP throws `TypeError` for the non-numeric string \"{}\" at a `float` parameter", + text.escape_debug() + )), + } +} + +/// A string that satisfies PHP's `is_numeric()` grammar, split into its numeric text and +/// whether that text spells a float. +struct NumericString<'a> { + /// The numeric run with PHP whitespace stripped from both ends. + text: &'a str, + /// True when the run contains a decimal point or a consumed exponent (PHP `IS_DOUBLE`). + is_float: bool, +} + +/// Scans a *fully* numeric string using PHP's numeric-string grammar, returning `None` when +/// trailing non-whitespace bytes remain. +/// +/// This is the compile-time twin of the runtime `__rt_php_num_scan` helper +/// (`crate::codegen_support::runtime::strings::php_num_scan`) restricted to +/// `is_numeric() === true`: leading and trailing PHP whitespace are allowed, an optional sign, +/// a mantissa with at least one digit, and an exponent only when a digit follows it. Hex, +/// underscore separators, `INF` and `NAN` are not part of the grammar. Parameter binding only +/// ever accepts fully numeric strings, so a leading-numeric string like `"42abc"` returns +/// `None` here and reaches PHP's `TypeError`. +fn scan_php_numeric_string(value: &str) -> Option> { + let bytes = value.as_bytes(); + let mut idx = 0; + while idx < bytes.len() && is_php_whitespace(bytes[idx]) { + idx += 1; + } + + let start = idx; + if idx < bytes.len() && matches!(bytes[idx], b'+' | b'-') { + idx += 1; + } + + let mut digits = 0; + while idx < bytes.len() && bytes[idx].is_ascii_digit() { + idx += 1; + digits += 1; + } + + let mut is_float = false; + if idx < bytes.len() && bytes[idx] == b'.' { + let mut probe = idx + 1; + while probe < bytes.len() && bytes[probe].is_ascii_digit() { + probe += 1; + digits += 1; + } + if digits > 0 { + idx = probe; + is_float = true; + } + } + if digits == 0 { + return None; + } + + if idx < bytes.len() && matches!(bytes[idx], b'e' | b'E') { + let mut probe = idx + 1; + if probe < bytes.len() && matches!(bytes[probe], b'+' | b'-') { + probe += 1; + } + let exponent_start = probe; + while probe < bytes.len() && bytes[probe].is_ascii_digit() { + probe += 1; + } + if probe > exponent_start { + idx = probe; + is_float = true; + } + } + + let end = idx; + while idx < bytes.len() && is_php_whitespace(bytes[idx]) { + idx += 1; + } + if idx != bytes.len() { + return None; + } + + Some(NumericString { + text: &value[start..end], + is_float, + }) +} + +/// Returns true for the bytes PHP's numeric-string grammar treats as leading/trailing space. +fn is_php_whitespace(byte: u8) -> bool { + matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r') +} + +/// Renders a float the way PHP spells it inside a diagnostic (`5.5`, `INF`, `NAN`). +fn php_float_text(value: f64) -> String { + if value.is_nan() { + return "NAN".to_string(); + } + if value.is_infinite() { + return if value.is_sign_negative() { "-INF" } else { "INF" }.to_string(); + } + format!("{}", value) +} + +/// Rewrites an argument whose parameter binding is decidable from its literal spelling alone, +/// before EIR lowering evaluates it. +/// +/// This covers the two bindings that must not lower the original argument at all: a +/// callable-name string becomes the equivalent first-class callable, and a constant bound to +/// `int`/`float` becomes the already-coerced literal. Scalar `(string)`/`(bool)` bindings are +/// applied to the lowered *value* instead (see `coerce_scalar_arg_to_param_storage`), because +/// they must also fire for runtime values whose type is only known after lowering. +/// +/// Returns `None` when the argument needs no pre-lowering rewrite. The checker has already +/// rejected every binding this can produce that would not resolve, so lowering can apply the +/// rewrite unconditionally. +pub(crate) fn rewrite_literal_param_binding(expected: &PhpType, arg: &Expr) -> Option { + let actual = literal_php_type(&arg.kind)?; + match classify_param_binding(expected, &actual, arg) { + ParamBinding::Const(kind) => Some(Expr::new(kind, arg.span)), + ParamBinding::Callable(target) => { + Some(Expr::new(ExprKind::FirstClassCallable(target), arg.span)) + } + _ => None, + } +} + +/// Returns the PHP type of an argument whose type is fixed by its literal spelling. +/// +/// Only the literals parameter binding can decide without inference are recognized; anything +/// else returns `None` so the caller falls back to a value-level binding after lowering. +fn literal_php_type(kind: &ExprKind) -> Option { + match kind { + ExprKind::StringLiteral(_) => Some(PhpType::Str), + ExprKind::FloatLiteral(_) => Some(PhpType::Float), + ExprKind::IntLiteral(_) => Some(PhpType::Int), + ExprKind::BoolLiteral(_) => Some(PhpType::Bool), + _ => None, + } +} + +/// Returns the explicit cast that implements a *value-level* scalar parameter binding. +/// +/// `actual` is the lowered argument's codegen type, so this fires for runtime values as well +/// as literals. Returns `None` when no total scalar coercion applies. +/// +/// The classifier is shared with the checker so the two cannot disagree; the placeholder +/// argument is inert because only the value-independent `ParamBinding::Cast` outcome is kept, +/// and every expression-sensitive outcome maps to `None` here. +pub(crate) fn scalar_param_cast(expected: &PhpType, actual: &PhpType) -> Option { + let placeholder = Expr::new(ExprKind::Null, crate::span::Span::dummy()); + match classify_param_binding(expected, actual, &placeholder) { + ParamBinding::Cast(target) => Some(target), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Builds a string-literal argument expression for the binding classifiers. + fn string_arg(text: &str) -> Expr { + Expr::new( + ExprKind::StringLiteral(text.to_string()), + crate::span::Span::dummy(), + ) + } + + /// Verifies the numeric-string scanner matches PHP's `is_numeric()` for the spellings + /// parameter binding depends on, including trailing whitespace and rejected garbage. + #[test] + fn numeric_string_scan_matches_php_is_numeric() { + assert!(scan_php_numeric_string("42").is_some()); + assert!(scan_php_numeric_string(" 42 ").is_some()); + assert!(scan_php_numeric_string("-4.5").is_some()); + assert!(scan_php_numeric_string("1e3").is_some()); + assert!(scan_php_numeric_string("42abc").is_none()); + assert!(scan_php_numeric_string("abc").is_none()); + assert!(scan_php_numeric_string("0x1A").is_none()); + assert!(scan_php_numeric_string("1_000").is_none()); + assert!(scan_php_numeric_string("").is_none()); + } + + /// Verifies an integral numeric string binds to `int` as a literal while a fractional one + /// is reported as PHP's deprecation and a non-numeric one as PHP's `TypeError`. + #[test] + fn string_to_int_binding_follows_php() { + assert_eq!( + const_string_to_int("42"), + ParamBinding::Const(ExprKind::IntLiteral(42)) + ); + assert_eq!( + const_string_to_int(" 42 "), + ParamBinding::Const(ExprKind::IntLiteral(42)) + ); + assert!(matches!( + const_string_to_int("4.5"), + ParamBinding::Deprecated(_) + )); + assert!(matches!( + const_string_to_int("42abc"), + ParamBinding::TypeError(_) + )); + } + + /// Verifies float literals bind to `int` only when they are integral and in range. + #[test] + fn float_to_int_binding_follows_php() { + assert_eq!( + const_float_to_int(5.0), + ParamBinding::Const(ExprKind::IntLiteral(5)) + ); + assert!(matches!(const_float_to_int(5.5), ParamBinding::Deprecated(_))); + assert!(matches!( + const_float_to_int(f64::NAN), + ParamBinding::TypeError(_) + )); + assert!(matches!( + const_float_to_int(1e20), + ParamBinding::TypeError(_) + )); + } + + /// Verifies `"Class::method"` splits into a static-method target and a plain name into a + /// function target, with a leading `\` dropped. + #[test] + fn callable_strings_split_into_targets() { + assert!(matches!( + callable_target_from_name("strtoupper"), + Some(CallableTarget::Function(_)) + )); + assert!(matches!( + callable_target_from_name("\\strtoupper"), + Some(CallableTarget::Function(_)) + )); + assert!(matches!( + callable_target_from_name("Formatter::wrap"), + Some(CallableTarget::StaticMethod { .. }) + )); + assert!(callable_target_from_name("").is_none()); + assert!(callable_target_from_name("::wrap").is_none()); + } + + /// Verifies a non-literal callable argument is reported as needing a runtime check rather + /// than being silently accepted into a callable slot that cannot be invoked. + #[test] + fn runtime_callable_string_is_not_bindable() { + let arg = Expr::new( + ExprKind::Variable("f".to_string()), + crate::span::Span::dummy(), + ); + let binding = classify_param_binding(&PhpType::Callable, &PhpType::Str, &arg); + assert!(matches!(binding, ParamBinding::NeedsRuntimeCheck(_))); + } + + /// Verifies `declare(strict_types=1)` keeps only the `int`→`float` widening across the full + /// 4x4 scalar matrix, matching PHP 8.4.20's accept/reject table. + #[test] + fn strict_types_keeps_only_the_int_to_float_widening() { + let scalars = [ + PhpType::Int, + PhpType::Float, + PhpType::Str, + PhpType::Bool, + ]; + for expected in &scalars { + for actual in &scalars { + let accepted = strict_param_binding_rejection(expected, actual).is_none(); + let should_accept = expected == actual + || (*expected == PhpType::Float && *actual == PhpType::Int); + assert_eq!( + accepted, should_accept, + "strict binding of {:?} into {:?}", + actual, expected + ); + } + } + } + + /// Verifies the strict rejection stays out of every non-scalar declaration, so `mixed`, + /// arrays, unions, `callable` and class types keep the acceptance rules they already had. + #[test] + fn strict_types_ignores_non_scalar_declarations() { + assert!(strict_param_binding_rejection(&PhpType::Mixed, &PhpType::Int).is_none()); + assert!(strict_param_binding_rejection(&PhpType::Int, &PhpType::Mixed).is_none()); + assert!(strict_param_binding_rejection(&PhpType::Callable, &PhpType::Str).is_none()); + assert!(strict_param_binding_rejection( + &PhpType::Array(Box::new(PhpType::Int)), + &PhpType::Str + ) + .is_none()); + assert!(strict_param_binding_rejection( + &PhpType::Union(vec![PhpType::Int, PhpType::Str]), + &PhpType::Bool + ) + .is_none()); + // `false` is a subtype of `bool`, not a distinct scalar for binding purposes. + assert!(strict_param_binding_rejection(&PhpType::Bool, &PhpType::False).is_none()); + } + + /// Verifies the strict diagnostic names PHP's `TypeError` types and suggests the cast that + /// makes the call legal, so the message is actionable rather than just a refusal. + #[test] + fn strict_types_diagnostic_names_the_php_type_error() { + let detail = strict_param_binding_rejection(&PhpType::Int, &PhpType::Str) + .expect("string into int is rejected under strict_types"); + assert!(detail.contains("declare(strict_types=1)"), "{}", detail); + assert!(detail.contains("must be of type int, string given"), "{}", detail); + assert!(detail.contains("`(int)` cast"), "{}", detail); + } + + /// Verifies a scalar bound to `string` becomes an explicit `(string)` cast, which is the + /// conversion elephc already implements PHP-exactly. + #[test] + fn scalar_to_string_binding_uses_the_cast() { + let arg = Expr::new(ExprKind::IntLiteral(42), crate::span::Span::dummy()); + assert_eq!( + classify_param_binding(&PhpType::Str, &PhpType::Int, &arg), + ParamBinding::Cast(CastType::String) + ); + assert_eq!( + classify_param_binding(&PhpType::Bool, &PhpType::Str, &string_arg("a")), + ParamBinding::Cast(CastType::Bool) + ); + } +} diff --git a/src/types/result.rs b/src/types/result.rs index 2e611b0a1e..e0928ff726 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -9,7 +9,7 @@ //! Key details: //! - Fields are consumed by optimizer, codegen, and linker setup; keep additions explicit and phase-owned. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::codegen::platform::{Platform, Target}; use crate::errors::{CompileError, CompileWarning}; @@ -87,6 +87,9 @@ pub struct CheckResult { pub builtin_call_types: HashMap, /// Fixed-point array-local storage contracts keyed by function-like scope and loop span. pub loop_storage_types: LoopStorageTypes, + /// `(function-like scope, local name)` pairs for `string` locals that are a `++`/`--` + /// target, so EIR lowering can give them boxed `Mixed` storage from their first store. + pub string_incdec_locals: HashSet<(String, String)>, } /// Runs type checking using the host platform (auto-detected from the build environment). diff --git a/src/types/string_constants.rs b/src/types/string_constants.rs new file mode 100644 index 0000000000..ee328815aa --- /dev/null +++ b/src/types/string_constants.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Defines PHP string-related integer constants exposed by elephc. +//! Keeps `str_pad()`'s padding-mode constants in one source of truth for type checking and codegen. +//! +//! Called from: +//! - `crate::types::checker` when registering predefined constants. +//! - `crate::codegen_support::prescan` when materializing constant literal values. +//! +//! Key details: +//! - Values must match PHP's string extension constants exactly: `str_pad()` validates its +//! `$pad_type` against this 0..=2 range and raises `ValueError` for anything else, so a +//! mismatch here would turn a valid PHP call into a runtime exception. + +/// Tuple of `(name, value)` pairs for PHP string integer constants. +/// +/// `str_pad()` uses these constants to select which side of the input is padded. +pub(crate) const STRING_INT_CONSTANTS: &[(&str, i64)] = &[ + ("STR_PAD_LEFT", 0), + ("STR_PAD_RIGHT", 1), + ("STR_PAD_BOTH", 2), +]; + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies the padding-mode constants carry php-src's exact values. + /// + /// `str_pad()`'s omitted-`$pad_type` default and its `ValueError` range check both assume + /// `STR_PAD_RIGHT == 1` and a contiguous `0..=2` range. + #[test] + fn str_pad_modes_match_php() { + assert_eq!( + STRING_INT_CONSTANTS, + &[ + ("STR_PAD_LEFT", 0), + ("STR_PAD_RIGHT", 1), + ("STR_PAD_BOTH", 2), + ] + ); + } + + /// Asserts no duplicate names exist in `STRING_INT_CONSTANTS`. + #[test] + fn no_duplicate_constant_names() { + let mut names: Vec<&str> = STRING_INT_CONSTANTS.iter().map(|(n, _)| *n).collect(); + names.sort_unstable(); + let len_before = names.len(); + names.dedup(); + assert_eq!(names.len(), len_before, "duplicate string constant name"); + } +} diff --git a/src/var_export_prelude.rs b/src/var_export_prelude.rs index a123072f2e..91659d672e 100644 --- a/src/var_export_prelude.rs +++ b/src/var_export_prelude.rs @@ -25,8 +25,29 @@ //! `sprintf("%.{p}e", ...)` until `(float)` of the result equals the input, then //! rebuilds the digit string per PHP's exponent thresholds — independent of the //! default `(string)`/`echo` precision used elsewhere. -//! - Objects are out of scope (PHP renders `\Class::__set_state(...)`); a non -//! scalar/array value renders as the empty string. +//! - Objects render exactly as PHP does: `stdClass` as `(object) array( … )`, any +//! other class as `\Class::__set_state(array( … ))`, and an enum case as +//! `\Enum::Case`. PHP's object layout is NOT the array layout — an entry key sits +//! at `indent + 3` (arrays use `indent + 2`) while the value and the closing line +//! keep the array indents — which is why the object branch does not reuse the +//! array branch's padding. Property visibility is deliberately absent: unlike +//! `print_r`, PHP's `var_export` prints the bare property name. +//! - Object properties are reached through four `internal: true` helpers +//! (`__elephc_object_is_enum`, `__elephc_object_prop_count`, +//! `__elephc_object_prop_name`, `__elephc_object_prop_value`) because elephc has +//! no `get_object_vars()`, no object-to-array cast, and no `foreach` over a plain +//! object — and because `enum_exists()` needs a string literal in AOT mode, so a +//! prelude holding a runtime `mixed` cannot ask whether it is an enum any other +//! way. They read the same per-class descriptor `print_r` and `var_dump` walk. +//! - A value that is neither scalar, array nor object renders as the empty string. +//! - KNOWN DIVERGENCE: dynamic (undeclared) properties are not exported, matching +//! what elephc's `var_dump`/`print_r` already do for the same objects. +//! - `__elephc_var_export_escape` takes `string`, NOT `mixed`, and every caller +//! casts into a `string` local first. Passing a `string` value to a `mixed` +//! parameter boxes it into a fresh Mixed cell that nothing releases, so the +//! `mixed` spelling leaked one heap block per escaped string — one per exported +//! string VALUE and one per exported string KEY, in every program, long before +//! objects were in scope. `var_export_and_strstr_result_tests` pins the loop. //! - The `$return` flag is FLAG-AWARE at the call site, mirroring `print_r`: `name_resolver` //! retargets a literal-flag call at [`RENDER_HELPER`] (`: string`) or [`ECHO_HELPER`] //! (prints, returns `null`), and only a runtime flag keeps the two-mode `var_export` body @@ -37,13 +58,15 @@ use crate::parser::ast::Program; mod detect; /// The elephc-PHP `var_export` prelude: the public `var_export($value, $return)` -/// entry point plus two internal helpers (`__elephc_var_export_str` renders a value -/// to its parsable text, `__elephc_var_export_escape` single-quote-escapes a string). +/// entry point plus the internal helpers — `__elephc_var_export_str` renders a value +/// to its parsable text, `__elephc_var_export_escape` single-quote-escapes a string, +/// `__elephc_var_export_float` reproduces `serialize_precision = -1`, and +/// `__elephc_var_export_prop` renders one object property (its own function so the +/// boxed property value is a short-lived local rather than a loop-carried one). /// The helpers are prefixed so they cannot collide with user code, and `var_export` /// itself is injected only when the user does not define their own. pub const VAR_EXPORT_PRELUDE_SRC: &str = r#" '; - if (is_array($v)) { + if (is_array($v) || is_object($v)) { $out = $out . "\n" . $pad . ' ' . __elephc_var_export_str($v, $indent + 2); } else { $out = $out . __elephc_var_export_str($v, $indent + 2); @@ -120,6 +152,36 @@ function __elephc_var_export_str(mixed $value, int $indent): string { $out = $out . $pad . ')'; return $out; } + if (is_object($value)) { + $class = get_class($value); + $pad = str_repeat(' ', $indent); + if (__elephc_object_is_enum($value)) { + $cases = __elephc_object_prop_count($value); + for ($c = 0; $c < $cases; $c++) { + if (__elephc_object_prop_name($value, $c) === 'name') { + return '\\' . $class . '::' . __elephc_object_prop_value($value, $c); + } + } + return '\\' . $class; + } + if ($class === 'stdClass') { + $out = "(object) array(\n"; + $close = ')'; + } else { + $out = '\\' . $class . "::__set_state(array(\n"; + $close = '))'; + } + $count = __elephc_object_prop_count($value); + for ($i = 0; $i < $count; $i++) { + $name = __elephc_object_prop_name($value, $i); + if ($name === '') { + continue; + } + $out = $out . $pad . ' ' . "'" . __elephc_var_export_escape($name) . "' => "; + $out = $out . __elephc_var_export_prop($value, $i, $indent, $pad) . ",\n"; + } + return $out . $pad . $close; + } return ''; } function __elephc_var_export_echo(mixed $value) { diff --git a/tests/array_result_type_tests.rs b/tests/array_result_type_tests.rs index 38ee3aeb5b..9901fc9d36 100644 --- a/tests/array_result_type_tests.rs +++ b/tests/array_result_type_tests.rs @@ -228,10 +228,18 @@ fn array_map_over_bool_returning_builtin_renders_php_style() { /// array's element type. The probe passes `$mapped[0]` — an `int[]` mapped through a /// `bool`-returning callback — into a `string` parameter: the diagnostic must name `Bool`. /// Before the fix it named `Int`, i.e. the input element type leaked through the map. +/// +/// The probe declares `strict_types=1` because bool → string is a legal *coercive* binding: +/// reference PHP accepts it and prints `string(1) "1"`, so under the default mode there is no +/// error left to read the element type out of. Strict mode is where PHP throws +/// `TypeError: ... must be of type string, bool given` — verified against php 8.4 — and it is +/// therefore the only probe that still observes the element type through a diagnostic. +/// The sibling `..._follows_a_string_callback` test needs no such directive: its `"n1"` is a +/// non-numeric string, which PHP rejects for an `int` parameter in both modes. #[test] fn array_map_result_element_type_is_the_callback_return_type() { let dir = make_test_dir("array_result_map_elemty"); - let src = " 0; } \ function want_string(string $s): string { return $s; } \ $mapped = array_map('is_pos', [1, 0]); \ diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 80124b1bb3..9b92b831f5 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -59,7 +59,17 @@ const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ "array_udiff", "array_uintersect", "array_walk_recursive", + "bindec", + "decbin", + "dechex", + "decoct", + "hexdec", + "join", + "octdec", "serialize", + "strncasecmp", + "strncmp", + "substr_count", "unserialize", "zval_free", "zval_pack", @@ -68,9 +78,12 @@ const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ ]; /// Eval supports these PHP optional parameters before the static backend does. +/// +/// `array_splice` left this list when the static backend gained PHP's `$replacement` +/// parameter, so both sides now declare the same four-parameter signature and the exact +/// shape comparison below applies to it. const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ "array_reverse", - "array_splice", "nl2br", "preg_match", "print_r", diff --git a/tests/codegen/array_basics.rs b/tests/codegen/array_basics.rs index 0f1724d0ed..aac30c0610 100644 --- a/tests/codegen/array_basics.rs +++ b/tests/codegen/array_basics.rs @@ -1579,3 +1579,52 @@ fn test_in_array_strict_distinguishes_bool_int_membership() { ); assert_eq!(out, "101010"); } + +/// Verifies `unset()` on a never-declared variable is accepted and is a silent no-op, the way +/// PHP treats it — `unset()` exists to name storage that may not be there. +#[test] +fn test_unset_never_declared_variable_is_a_noop() { + let out = compile_and_run( + r#"(asm: &'a str, label: &str) -> &'a str { + let marker = format!("{label}:"); + let start = asm + .find(&marker) + .unwrap_or_else(|| panic!("missing assembly label {label}")); + let rest = &asm[start..]; + let end = rest.find("\n\n").unwrap_or(rest.len()); + &rest[..end] +} + +/// Verifies `array_fill()` with a count whose `count * 8` payload size wraps the machine word +/// never reaches the allocator at all: the count is past `INT_MAX`, which reference PHP rejects +/// with a catchable `ValueError` before it looks at memory, so the lowering guard raises that +/// instead of letting `__rt_array_new` report the array-size fatal. See `arrays::size_bounds` for +/// the full bounds matrix; the `__rt_array_new` guard behind it is pinned by +/// `test_x86_64_runtime_array_new_carries_size_guard`. +#[test] +fn test_array_fill_overflowing_count_is_fatal() { + let err = compile_and_run_expect_failure( + " $v) { echo "$k=$v,"; } +echo "|"; +echo implode(",", range(1, 5)), "|"; +echo implode(",", range(5, 1)), "|"; +echo implode(",", array_pad([1, 2], 5, 9)), "|"; +echo implode(",", array_pad([1, 2], -5, 9)); +"#, + ); + assert_eq!( + out, + "100:7:7|5=x,6=x,7=x,|1,2,3,4,5|5,4,3,2,1|1,2,9,9,9|9,9,9,1,2" + ); +} + +/// Regression guard for the clean heap-exhaustion path: a capacity that is large but whose payload +/// size is representable must still report heap exhaustion, not the new array-size fatal. +#[test] +fn test_large_but_representable_allocation_still_reports_heap_exhaustion() { + let err = compile_and_run_expect_failure( + " $q <=> $p); } +$v = [3,1,2]; $va = $v; u($v); echo implode(",", $v), "|", implode(",", $va), "\n"; +function k(array &$a) { ksort($a); } +$m = ["b"=>2,"a"=>1]; $ma = $m; k($m); echo implode(",", array_keys($m)), "|", implode(",", array_keys($ma)), "\n"; +function ms(array &$p, array &$q) { array_multisort($p, $q); } +$o = [3,1,2]; $oo = [30,10,20]; $oa = $o; ms($o, $oo); echo implode(",", $o), "|", implode(",", $oa), "\n"; +"#, + ); + assert_eq!( + out, + r#"1,2,3|3,1,2 +3,2,1|3,1,2 +a,b|b,a +1,2,3|3,1,2 +"# + ); +} + +/// Verifies an associative insert through a by-reference parameter reaches the caller's table. +/// +/// `$a["c"] = 3` splits the shared table with `__rt_hash_ensure_unique` and can reallocate it, +/// so the hash lowering needs the same ref-cell write-back the indexed builtins do. +#[test] +fn test_hash_insert_on_by_ref_parameter_matches_php() { + let out = compile_and_run( + r#"1,"b"=>2]; $na = $n; hs($n); echo implode(",", array_keys($n)), "|", implode(",", array_keys($na)), "\n"; +"#, + ); + assert_eq!(out, "a,b,c|a,b\n"); +} + +/// Verifies the whole by-reference receiver matrix leaves the heap balanced. +/// +/// Republishing a relocated pointer through a ref cell releases whatever the slot held before, +/// so a write-back that dropped or double-counted the previous owner shows up here. +#[test] +fn test_by_ref_parameter_receivers_leave_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#"items, fn($x, $y) => $x <=> $y); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "1,2,3"); +} + +/// The same regression for a string-element array, so the fix is not specific to the +/// integer sort helper. +#[test] +fn test_usort_on_instance_property_sorts_strings_in_place() { + let out = compile_and_run( + r#"items, fn($x, $y) => strcmp($x, $y)); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "apple,fig,pear"); +} + +/// `sort()` and `rsort()` both mutate an instance property, confirming the rewrite is driven +/// by the by-reference parameter rather than by one builtin's lowering. +#[test] +fn test_sort_and_rsort_on_instance_property() { + let out = compile_and_run( + r#"items); +echo implode(",", $b->items), "|"; +rsort($b->items); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "1,2,3|3,2,1"); +} + +/// The whole structural-mutator family on one instance property: push, pop, shift, unshift, +/// and splice each have to observe and update the same property storage in sequence. +#[test] +fn test_structural_mutators_on_instance_property() { + let out = compile_and_run( + r#"items, 9); +echo implode(",", $b->items), "|"; +echo array_pop($b->items), "|"; +echo array_shift($b->items), "|"; +array_unshift($b->items, 7); +echo implode(",", $b->items), "|"; +array_splice($b->items, 1, 1); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "3,1,2,9|9|3|7,1,2|7,2"); +} + +/// A static property receiver: `sort()` mutated it only while it was unaliased, and +/// `array_push()`/`usort()` on it were silent no-ops once a copy existed. +#[test] +fn test_sort_family_on_static_property() { + let out = compile_and_run( + r#" $y <=> $x); +echo implode(",", B::$items); +"#, + ); + assert_eq!(out, "1,2,3|3,2,1,0"); +} + +/// A hash element holding a nested array: `sort($m["k"])` used to sort a discarded copy. +#[test] +fn test_sort_on_string_keyed_array_element() { + let out = compile_and_run( + r#" [3,1,2], "j" => [5,4]]; +sort($m["k"]); +rsort($m["j"]); +echo implode(",", $m["k"]), "|", implode(",", $m["j"]); +"#, + ); + assert_eq!(out, "1,2,3|5,4"); +} + +/// An indexed element holding a nested array. This shape previously failed EIR validation +/// because the element-address by-reference path only models scalar element cells. +#[test] +fn test_sort_on_indexed_array_element() { + let out = compile_and_run( + r#" $x <=> $y); +array_push($a[1], 7); +echo implode(",", $a[0]), "|", implode(",", $a[1]); +"#, + ); + assert_eq!(out, "1,2,3|9,8,7"); +} + +/// Copy-on-write on a property receiver: PHP separates the array before sorting, so the +/// alias taken before the call keeps the original element order. +#[test] +fn test_sort_on_instance_property_respects_copy_on_write() { + let out = compile_and_run( + r#"items; +usort($b->items, fn($x, $y) => $x <=> $y); +echo implode(",", $b->items), "|", implode(",", $copy); +"#, + ); + assert_eq!(out, "1,2,3|3,1,2"); +} + +/// Copy-on-write on a static-property receiver. A static-property load carries no reference +/// of its own, so an implementation that moved the borrowed pointer into a temporary would +/// free the array the alias still holds when the write-back released the previous occupant. +#[test] +fn test_sort_on_static_property_respects_copy_on_write() { + let out = compile_and_run( + r#" [3,1,2]]; +$copy = $m["k"]; +sort($m["k"]); +echo implode(",", $m["k"]), "|", implode(",", $copy); +"#, + ); + assert_eq!(out, "1,2,3|3,1,2"); +} + +/// A `$this->prop` receiver inside a method body. +#[test] +fn test_usort_on_this_property_inside_method() { + let out = compile_and_run( + r#"items, fn($x, $y) => $x <=> $y); } +} +$b = new B(); +$b->sortItems(); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "1,2,3"); +} + +/// A two-link property chain (`$outer->inner->items`), so the receiver resolution walks more +/// than one property hop before it finds the array storage. +#[test] +fn test_sort_on_nested_property_chain() { + let out = compile_and_run( + r#"inner = new Inner(); } } +$o = new Outer(); +sort($o->inner->items); +echo implode(",", $o->inner->items); +"#, + ); + assert_eq!(out, "1,2,3"); +} + +/// The element index of a by-reference place is evaluated exactly once, even though the place +/// is read before the call and written after it. +#[test] +fn test_by_ref_element_index_is_evaluated_once() { + let out = compile_and_run( + r#"items; +sort(array: $b->items); +echo implode(",", $b->items), "|", implode(",", $copy), "|"; +$c = new B(); +usort(callback: fn($x, $y) => $y <=> $x, array: $c->items); +echo implode(",", $c->items); +"#, + ); + assert_eq!(out, "1,2,3|3,1,2|3,2,1"); +} + +/// `shuffle()` on a property permutes the property's own storage. The permutation is random, +/// so the assertion checks the multiset and length rather than an order. +#[test] +fn test_shuffle_on_instance_property_permutes_property_storage() { + let out = compile_and_run( + r#"items); +$c = $b->items; +sort($c); +echo implode(",", $c), "|", count($b->items); +"#, + ); + assert_eq!(out, "1,2,3,4,5|5"); +} + +/// `array_unshift()` on a full array must grow the payload before shifting. Without the +/// growth it wrote one element past the allocation and left `length > capacity`, so the next +/// copy-on-write split produced an over-long copy whose tail read adjacent heap header words. +#[test] +fn test_array_unshift_grows_before_prepending() { + let out = compile_and_run( + r#"items, 7); +rsort($b->items); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "7,3,2,1,0"); +} diff --git a/tests/codegen/arrays/callbacks.rs b/tests/codegen/arrays/callbacks.rs index e999dda298..881fbe9dcc 100644 --- a/tests/codegen/arrays/callbacks.rs +++ b/tests/codegen/arrays/callbacks.rs @@ -1402,3 +1402,363 @@ echo count(array_map(function (int $n): int { return $n; }, build())); ); assert_eq!(out, "0"); } + +/// Verifies an untyped arrow-function predicate passed to `array_filter` inherits the +/// indexed array's `string` element type, so `strlen()` in its body compiles and runs. +#[test] +fn test_array_filter_untyped_closure_param_inherits_string_element() { + let out = compile_and_run( + r#" strlen($v) > 3); +echo implode(",", $r); +"#, + ); + assert_eq!(out, "banana,apple"); +} + +/// Verifies an untyped arrow-function callback passed to `array_map` inherits the indexed +/// array's `string` element type. +#[test] +fn test_array_map_untyped_closure_param_inherits_string_element() { + let out = compile_and_run( + r#" strtoupper($v), $w); +echo implode(",", $r); +"#, + ); + assert_eq!(out, "BANANA,APPLE"); +} + +// --- usort over indexed string arrays (16-byte descriptor slots) --- + +/// Verifies the audit repro: an untyped arrow-function comparator passed to `usort` +/// inherits the indexed array's `string` element type and the backend reorders the +/// 16-byte string descriptor slots through `__rt_usort_str`. +#[test] +fn test_usort_string_array_untyped_arrow_comparator() { + let out = compile_and_run( + r#" strlen($a) <=> strlen($b)); +echo implode(",", $words); +"#, + ); + assert_eq!(out, "apple,banana"); +} + +/// Verifies `usort` orders a string array alphabetically through a `strcmp()` comparator. +#[test] +fn test_usort_string_array_strcmp_ascending() { + let out = compile_and_run( + r#" strcmp($a, $b)); +echo implode("|", $w); +"#, + ); + assert_eq!(out, "apple|banana|date|fig|pear"); +} + +/// Verifies a reversed `strcmp()` comparator produces descending string order. +#[test] +fn test_usort_string_array_strcmp_descending() { + let out = compile_and_run( + r#" strcmp($b, $a)); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "pear,fig,date,banana"); +} + +/// Verifies the string sort is stable like PHP 8's `usort`: elements the comparator +/// reports equal keep their original relative order. +#[test] +fn test_usort_string_array_is_stable_for_equal_elements() { + let out = compile_and_run( + r#" strlen($a) <=> strlen($b)); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "c,e,bb,aa,dd"); +} + +/// Verifies `usort` renumbers a string array's keys from zero, matching PHP. +#[test] +fn test_usort_string_array_reindexes_keys() { + let out = compile_and_run( + r#" strcmp($a, $b)); +foreach ($w as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "0=alpha;1=mid;2=zeta;"); +} + +/// Verifies a single-element string array survives `usort` unchanged. +#[test] +fn test_usort_string_array_single_element() { + let out = compile_and_run( + r#" strcmp($a, $b)); +echo count($w), "|", implode(",", $w); +"#, + ); + assert_eq!(out, "1|only"); +} + +/// Verifies an empty string array is a no-op for `usort`. +/// Fixture: `array_pop()` empties a string array so the element type stays `string`. +#[test] +fn test_usort_string_array_empty() { + let out = compile_and_run( + r#" strcmp($a, $b)); +echo count($w), "|", implode(",", $w); +"#, + ); + assert_eq!(out, "0|"); +} + +/// Verifies empty-string elements sort correctly, guarding the zero-length descriptor. +#[test] +fn test_usort_string_array_with_empty_string_element() { + let out = compile_and_run( + r#" strlen($a) <=> strlen($b)); +echo "[", implode("|", $w), "]"; +"#, + ); + assert_eq!(out, "[|a|bb]"); +} + +/// Verifies a string-literal comparator name resolves to a user function for string sorts. +#[test] +fn test_usort_string_array_named_function_comparator() { + let out = compile_and_run( + r#" strlen($b); } +$w = ["xxx", "y", "zz"]; +usort($w, 'cmp_len'); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "y,zz,xxx"); +} + +/// Verifies a runtime string variable naming the comparator dispatches through the +/// string-callback descriptor path for a string receiver. +#[test] +fn test_usort_string_array_runtime_string_comparator() { + let out = compile_and_run( + r#" strlen($b); } +$name = "cmp_len"; +$w = ["xxx", "y", "zz"]; +usort($w, $name); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "y,zz,xxx"); +} + +/// Verifies a `callable`-typed variable holding a closure sorts a string array through +/// the descriptor-callback runtime path. +#[test] +fn test_usort_string_array_callable_variable_comparator() { + let out = compile_and_run( + r#" strcmp($a, $b); +$w = ["c", "a", "b"]; +usort($w, $cb); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "a,b,c"); +} + +/// Verifies a closure comparator with a `use` capture reaches the string sorter with its +/// capture environment intact. +#[test] +fn test_usort_string_array_closure_capture_comparator() { + let out = compile_and_run( + r#" strcmp($a, $b)); +$copy = $words; +usort($copy, fn($a, $b) => strlen($a) <=> strlen($b)); +echo implode(",", $copy), "/", implode(",", $words); +"#, + ); + assert_eq!(out, "fig,apple,banana,cherry/apple,banana,cherry,fig"); +} + +/// Verifies a static-method first-class callable comparator sorts a string array through +/// the two-string callback wrapper ABI. +#[test] +fn test_usort_string_array_static_method_comparator() { + let out = compile_and_run( + r#"dir * strcmp($a, $b); } +} +$o = new Cmp(-1); +$w = ["a", "c", "b", "d"]; +usort($w, $o->cmp(...)); +echo implode(",", $w); +"#, + ); + assert_eq!(out, "d,c,b,a"); +} + +// --- array_reduce over indexed string arrays (16-byte descriptor slots) --- + +/// Verifies `array_reduce()` folds an indexed string array into an integer accumulator, +/// with the untyped arrow-function parameters typed from the array element. +#[test] +fn test_array_reduce_string_array_untyped_arrow_callback() { + let out = compile_and_run( + r#" $c + strlen($v), 0); +"#, + ); + assert_eq!(out, "6"); +} + +/// Verifies a string-literal callback name folds a string array through the runtime helper. +#[test] +fn test_array_reduce_string_array_named_function_callback() { + let out = compile_and_run( + r#" $c + strlen($v); +$w = ["a", "bb", "ccc"]; +echo array_reduce($w, $cb, 0); +"#, + ); + assert_eq!(out, "6"); +} + +/// Verifies a closure callback with a `use` capture reaches the string-array reducer with +/// its capture environment intact, starting from a non-zero initial accumulator. +#[test] +fn test_array_reduce_string_array_closure_capture_callback() { + let out = compile_and_run( + r#" $c + strlen($v), 7); +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies a single-element string array folds exactly once, including an empty-string +/// element whose zero-length descriptor must still reach the callback. +#[test] +fn test_array_reduce_string_array_single_empty_string_element() { + let out = compile_and_run( + r#" $c + strlen($v) + 1, 0); +"#, + ); + assert_eq!(out, "1"); +} diff --git a/tests/codegen/arrays/closure_literal_returns.rs b/tests/codegen/arrays/closure_literal_returns.rs new file mode 100644 index 0000000000..503c717c49 --- /dev/null +++ b/tests/codegen/arrays/closure_literal_returns.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Regression tests for the array-storage type stamped on an array literal that a closure +//! returns directly: nested literals, an array-typed parameter, an associative literal, and a +//! literal containing a spread. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected value is verbatim `LC_ALL=C php` output from PHP 8.4.20. +//! - The closure return-type inference in `src/ir_lower/function.rs` and the literal typing in +//! `src/ir_lower/expr` (`array_literal_type_for_ir` / `assoc_array_literal_type_for_ir`) type +//! the same literal and must agree: `lower_return_expr` feeds the inferred return element type +//! back into `lower_array_literal_with_expected_type`, and the caller reads the returned +//! array through the same signature metadata. When the inference fell back to the syntactic +//! `int` default, `function (array $xs) { return [$xs]; }` returned `[1]` instead of the +//! nested array and `function (string $s) { return ['k' => $s]; }` read the string payload +//! back as a raw pointer-sized integer. +//! - The spread fixture pins the caller-side stamp: the body already built `array` +//! through the spread lowering while the signature still advertised `array`. + +use crate::support::*; + +/// Verifies a nested array literal inside a closure-returned literal keeps its inner element +/// types, including a string in the inner array and at the outer level. +#[test] +fn test_closure_returns_nested_array_literal_of_mixed_params() { + let out = compile_and_run( + r#"\n array(2) {\n [0]=>\n int(1)\n [1]=>\n string(1) \"z\"\n }\n [1]=>\n string(1) \"z\"\n}\n" + ); +} + +/// Verifies an `array`-typed parameter wrapped in a returned literal stays an array instead of +/// being cast to the syntactic `int` default. +#[test] +fn test_closure_returns_array_literal_wrapping_array_param() { + let out = compile_and_run( + r#"\n array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n }\n}\n" + ); +} + +/// Verifies an associative literal returned directly from a closure stamps its value slot from +/// the parameter type, so reading the key back yields the string rather than a raw integer. +#[test] +fn test_closure_returns_assoc_literal_of_typed_param() { + let out = compile_and_run( + r#" $s]; }; +$r = $f("yo"); +var_dump($r['k']); +var_dump($r); +"#, + ); + assert_eq!( + out, + "string(2) \"yo\"\narray(1) {\n [\"k\"]=>\n string(2) \"yo\"\n}\n" + ); +} + +/// Verifies a returned literal that spreads an array parameter and appends a `mixed` argument +/// keeps the appended string, pinning agreement between the callee's spread lowering and the +/// signature the caller reads. +#[test] +fn test_closure_returns_spread_array_literal() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n int(2)\n [2]=>\n string(1) \"s\"\n}\n" + ); +} diff --git a/tests/codegen/arrays/indexed.rs b/tests/codegen/arrays/indexed.rs index eac2ce7a10..2529aba5ce 100644 --- a/tests/codegen/arrays/indexed.rs +++ b/tests/codegen/arrays/indexed.rs @@ -13,10 +13,18 @@ use crate::support::*; mod aggregates; #[path = "indexed/heterogeneous.rs"] mod heterogeneous; +#[path = "indexed/pad_bounds.rs"] +mod pad_bounds; #[path = "indexed/search_merge_union.rs"] mod search_merge_union; +#[path = "indexed/slice_bounds.rs"] +mod slice_bounds; #[path = "indexed/slice_stack_range.rs"] mod slice_stack_range; +#[path = "indexed/splice_replacement.rs"] +mod splice_replacement; +#[path = "indexed/splice_strings.rs"] +mod splice_strings; #[path = "indexed/set_ops.rs"] mod set_ops; #[path = "indexed/shape_transforms.rs"] diff --git a/tests/codegen/arrays/indexed/pad_bounds.rs b/tests/codegen/arrays/indexed/pad_bounds.rs new file mode 100644 index 0000000000..09748a0d57 --- /dev/null +++ b/tests/codegen/arrays/indexed/pad_bounds.rs @@ -0,0 +1,212 @@ +//! Purpose: +//! Regression tests for PHP's `array_pad()` `$length` bounds: the sign/magnitude matrix that +//! decides between padding left, padding right, and copying, and the oversized magnitudes that +//! reference PHP rejects with a catchable `ValueError` instead of building an array. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected string in this file is verbatim `LC_ALL=C php` 8.4 output for the same fixture. +//! - The matrices assert `count()` on every result, so a runtime helper that publishes a negative +//! logical length in the array header can never pass again. +//! - `PHP_INT_MIN` is the sharpest case: its magnitude is not representable, so the pre-fix helpers +//! negated it straight back to a negative "length" instead of reporting an error. +//! - The scalar fixtures exercise `__rt_array_pad`; the `[[1], [2]]` fixtures exercise +//! `__rt_array_pad_refcounted`. Both take `$length` in the second ABI argument register, which is +//! where the shared lowering guard inspects it. +//! - Lengths stay literal or plainly int-typed: an arithmetic expression over `$argc` produces a +//! boxed `Mixed` local, and passing one of those to any runtime builtin is a separate, +//! pre-existing lowering gap that would mask the behavior under test. +//! - The maximum accepted magnitude (`1073741824`) is deliberately never executed: reference PHP +//! accepts it and then fails on memory, and so does elephc, but the attempt would ask this +//! process for an 8 GiB payload. + +use super::*; + +/// php-src's verbatim `ValueError` message for an `array_pad()` `$length` past the maximum +/// allowed array size, repeated once per rejected fixture length. +const PAD_LENGTH_VALUE_ERROR: &str = + "ValueError: array_pad(): Argument #2 ($length) must not exceed the maximum allowed array size"; + +/// Verifies the full sign/magnitude matrix for a scalar source array: a magnitude larger than the +/// source pads (right for a positive `$length`, left for a negative one), a magnitude at or below +/// the source length copies, and the source array itself is never mutated. +#[test] +fn test_array_pad_length_matrix_matches_php() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } +} +echo count($a), "\n"; +"#, + ); + assert_eq!( + out, + format!("{PAD_LENGTH_VALUE_ERROR}\n").repeat(6) + "2\n" + ); +} + +/// Regression: the refcounted pad helper is guarded by the same bound, so an oversized `$length` +/// over a nested-array source throws instead of asking the allocator for an impossible payload. +#[test] +fn test_array_pad_refcounted_oversized_length_throws_catchable_value_error() { + let out = compile_and_run( + r#"getMessage(), "\n"; +} +try { + $r = array_pad($b, -2000000000, [0]); + echo "no throw ", count($r), "\n"; +} catch (ValueError $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +echo count($b), "\n"; +"#, + ); + assert_eq!( + out, + format!("{PAD_LENGTH_VALUE_ERROR}\n").repeat(2) + "2\n" + ); +} + +/// Verifies the guard survives dead-code elimination and the try-prefix hoist: a discarded +/// `array_pad()` result still throws, and the throw still lands inside the enclosing `try`. +/// +/// `RuntimeFnId::ArrayPad` used to report no effects at all, so the optimizer treated the call as a +/// removable pure expression and hoisted it out of the `try` body it belongs to. +#[test] +fn test_array_pad_oversized_length_throws_from_discarded_result_inside_try() { + let out = compile_and_run( + r#"getMessage(), "\n"; +} +echo "after\n"; +"#, + ); + assert_eq!(out, format!("{PAD_LENGTH_VALUE_ERROR}\nafter\n")); +} + +/// Verifies an uncaught oversized `$length` terminates with the PHP-shaped uncaught diagnostic +/// rather than an allocator fatal or a silent out-of-bounds array. +#[test] +fn test_array_pad_oversized_length_uncaught_is_fatal() { + let err = compile_and_run_expect_failure( + " $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "a=2;b=1;"); +} + +/// Tests `array_count_values()` over an integer-valued indexed array. +#[test] +fn test_array_count_values_integers() { + let out = compile_and_run( + r#" $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "1=2;2=1;3=3;"); +} + +/// Verifies `array_count_values()` resolves case-insensitively, namespaced, and by named argument. +#[test] +fn test_array_count_values_case_insensitive_namespaced_and_named_args() { + let out = compile_and_run( + r#" $v) { echo $k, "=", $v, ";"; } +echo "|"; +foreach ($s as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "z=2;|5=2;6=1;"); +} + +/// Tests `array_count_values()` over an ASSOCIATIVE source: the source KEYS are discarded and +/// the source VALUES become the tally keys. +#[test] +fn test_array_count_values_assoc_source() { + let out = compile_and_run( + r#" "x", "k2" => "y", "k3" => "x"]; +$r = array_count_values($src); +foreach ($r as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "x=2;y=1;"); +} + +/// Verifies PHP's numeric-string key collapsing: `"10"` and `10` share one tally while the +/// non-canonical `"010"` stays a distinct string key. +#[test] +fn test_array_count_values_numeric_string_keys_collapse() { + let out = compile_and_run( + r#" $v) { echo var_export($k, true), "=", $v, ";"; } +"#, + ); + assert_eq!(out, "10=2;'010'=1;"); +} + +/// Verifies `array_count_values()` tallies runtime-built strings, so the call cannot be folded +/// into a literal and the `__rt_array_count_values` lowering is exercised. +#[test] +fn test_array_count_values_runtime_values() { + let out = compile_and_run( + r#" $v) { echo $k, "=", $v, ";"; } +echo "|", count($r); +"#, + ); + assert_eq!(out, "n0=2;z=1;|2"); +} diff --git a/tests/codegen/arrays/indexed/slice_bounds.rs b/tests/codegen/arrays/indexed/slice_bounds.rs new file mode 100644 index 0000000000..508a07e9e9 --- /dev/null +++ b/tests/codegen/arrays/indexed/slice_bounds.rs @@ -0,0 +1,705 @@ +//! Purpose: +//! Regression tests for PHP's `array_slice()`/`array_splice()` offset and length window +//! arithmetic, covering the negative-`$length` semantics that the runtime helpers used to +//! encode with an ambiguous `-1` "until the end" sentinel. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected string in this file is verbatim `LC_ALL=C php` 8.4 output for the same fixture. +//! - The matrices assert `count()` on every result, so a runtime helper that publishes a negative or +//! over-large logical length in the array header can never pass again. +//! - Fixtures drive the offsets/lengths through `foreach` over literal int arrays instead of a helper +//! function, because passing a freshly sliced array straight into a user function hits an unrelated +//! pre-existing ownership gap that would mask the behavior under test. +//! - The scalar fixtures exercise `__rt_array_slice`/`__rt_array_splice`, the `[[1], ...]` fixtures +//! exercise the refcounted variants, and the `$m["arr"]` and untyped-parameter fixtures exercise +//! the boxed-`Mixed` path. +//! - The untyped-parameter fixtures also pin the EIR result LAYOUT of a boxed-`Mixed` slice: the +//! checker specializes `function top($scores)` from its call site, EIR gives every undeclared +//! parameter the boxed-`Mixed` ABI contract, and the slice result must follow the operands rather +//! than the checker's narrower call-site type. +//! - The `PHP_INT_MAX`/`PHP_INT_MIN` fixture derives its bounds from `$argc` so the frontend cannot +//! fold the extreme offsets and lengths away before they reach the runtime helpers. + +use super::*; + +/// Regression: `array_slice()` with a negative `$length` must stop that many elements before the +/// end of the array and clamp to an empty result, never report a negative `count()`. +/// +/// `array_slice([1,2,3,4], 0, -10)` used to return an array whose header claimed `-10` elements and +/// `array_slice([1,2,3,4], 2, -1)` used to return 2 elements because `-1` doubled as the runtime +/// "slice to the end" sentinel. +#[test] +fn test_array_slice_negative_length_clamps_instead_of_reporting_negative_count() { + let out = compile_and_run( + r#" $v) { echo " ", $k, "=", $v; } + echo "\n"; + $r = array_slice([1, 2, 3, 4], $o, null); + echo "off=", $o, " len=null cnt=", count($r); + foreach ($r as $k => $v) { echo " ", $k, "=", $v; } + echo "\n"; + foreach ($lens as $l) { + $r = array_slice([1, 2, 3, 4], $o, $l); + echo "off=", $o, " len=", $l, " cnt=", count($r); + foreach ($r as $k => $v) { echo " ", $k, "=", $v; } + echo "\n"; + } +} +"#, + ); + assert_eq!( + out, + r#"off=-10 len=omit cnt=4 0=1 1=2 2=3 3=4 +off=-10 len=null cnt=4 0=1 1=2 2=3 3=4 +off=-10 len=-10 cnt=0 +off=-10 len=-4 cnt=0 +off=-10 len=-3 cnt=1 0=1 +off=-10 len=-1 cnt=3 0=1 1=2 2=3 +off=-10 len=0 cnt=0 +off=-10 len=1 cnt=1 0=1 +off=-10 len=3 cnt=3 0=1 1=2 2=3 +off=-10 len=4 cnt=4 0=1 1=2 2=3 3=4 +off=-10 len=10 cnt=4 0=1 1=2 2=3 3=4 +off=-4 len=omit cnt=4 0=1 1=2 2=3 3=4 +off=-4 len=null cnt=4 0=1 1=2 2=3 3=4 +off=-4 len=-10 cnt=0 +off=-4 len=-4 cnt=0 +off=-4 len=-3 cnt=1 0=1 +off=-4 len=-1 cnt=3 0=1 1=2 2=3 +off=-4 len=0 cnt=0 +off=-4 len=1 cnt=1 0=1 +off=-4 len=3 cnt=3 0=1 1=2 2=3 +off=-4 len=4 cnt=4 0=1 1=2 2=3 3=4 +off=-4 len=10 cnt=4 0=1 1=2 2=3 3=4 +off=-3 len=omit cnt=3 0=2 1=3 2=4 +off=-3 len=null cnt=3 0=2 1=3 2=4 +off=-3 len=-10 cnt=0 +off=-3 len=-4 cnt=0 +off=-3 len=-3 cnt=0 +off=-3 len=-1 cnt=2 0=2 1=3 +off=-3 len=0 cnt=0 +off=-3 len=1 cnt=1 0=2 +off=-3 len=3 cnt=3 0=2 1=3 2=4 +off=-3 len=4 cnt=3 0=2 1=3 2=4 +off=-3 len=10 cnt=3 0=2 1=3 2=4 +off=-1 len=omit cnt=1 0=4 +off=-1 len=null cnt=1 0=4 +off=-1 len=-10 cnt=0 +off=-1 len=-4 cnt=0 +off=-1 len=-3 cnt=0 +off=-1 len=-1 cnt=0 +off=-1 len=0 cnt=0 +off=-1 len=1 cnt=1 0=4 +off=-1 len=3 cnt=1 0=4 +off=-1 len=4 cnt=1 0=4 +off=-1 len=10 cnt=1 0=4 +off=0 len=omit cnt=4 0=1 1=2 2=3 3=4 +off=0 len=null cnt=4 0=1 1=2 2=3 3=4 +off=0 len=-10 cnt=0 +off=0 len=-4 cnt=0 +off=0 len=-3 cnt=1 0=1 +off=0 len=-1 cnt=3 0=1 1=2 2=3 +off=0 len=0 cnt=0 +off=0 len=1 cnt=1 0=1 +off=0 len=3 cnt=3 0=1 1=2 2=3 +off=0 len=4 cnt=4 0=1 1=2 2=3 3=4 +off=0 len=10 cnt=4 0=1 1=2 2=3 3=4 +off=1 len=omit cnt=3 0=2 1=3 2=4 +off=1 len=null cnt=3 0=2 1=3 2=4 +off=1 len=-10 cnt=0 +off=1 len=-4 cnt=0 +off=1 len=-3 cnt=0 +off=1 len=-1 cnt=2 0=2 1=3 +off=1 len=0 cnt=0 +off=1 len=1 cnt=1 0=2 +off=1 len=3 cnt=3 0=2 1=3 2=4 +off=1 len=4 cnt=3 0=2 1=3 2=4 +off=1 len=10 cnt=3 0=2 1=3 2=4 +off=3 len=omit cnt=1 0=4 +off=3 len=null cnt=1 0=4 +off=3 len=-10 cnt=0 +off=3 len=-4 cnt=0 +off=3 len=-3 cnt=0 +off=3 len=-1 cnt=0 +off=3 len=0 cnt=0 +off=3 len=1 cnt=1 0=4 +off=3 len=3 cnt=1 0=4 +off=3 len=4 cnt=1 0=4 +off=3 len=10 cnt=1 0=4 +off=4 len=omit cnt=0 +off=4 len=null cnt=0 +off=4 len=-10 cnt=0 +off=4 len=-4 cnt=0 +off=4 len=-3 cnt=0 +off=4 len=-1 cnt=0 +off=4 len=0 cnt=0 +off=4 len=1 cnt=0 +off=4 len=3 cnt=0 +off=4 len=4 cnt=0 +off=4 len=10 cnt=0 +off=10 len=omit cnt=0 +off=10 len=null cnt=0 +off=10 len=-10 cnt=0 +off=10 len=-4 cnt=0 +off=10 len=-3 cnt=0 +off=10 len=-1 cnt=0 +off=10 len=0 cnt=0 +off=10 len=1 cnt=0 +off=10 len=3 cnt=0 +off=10 len=4 cnt=0 +off=10 len=10 cnt=0 +"# + ); +} + +/// Regression: the full `array_splice()` `$offset` x `$length` matrix must match PHP. +/// +/// Asserts both the removed-elements array and what stays in the spliced source array. A negative +/// `$length` used to make the ARM64 helper walk its compaction cursor backwards and publish a source +/// length longer than the allocation, so the source contents are checked on every row. +#[test] +fn test_array_splice_offset_length_matrix_matches_php() { + let out = compile_and_run( + r#" $v) { echo " ", $k, "=", $v; } + echo " a=", count($a); + foreach ($a as $k => $v) { echo " ", $k, "=", $v; } + echo "\n"; + $a = [1, 2, 3, 4]; + $r = array_splice($a, $o, null); + echo "off=", $o, " len=null r=", count($r); + foreach ($r as $k => $v) { echo " ", $k, "=", $v; } + echo " a=", count($a); + foreach ($a as $k => $v) { echo " ", $k, "=", $v; } + echo "\n"; + foreach ($lens as $l) { + $a = [1, 2, 3, 4]; + $r = array_splice($a, $o, $l); + echo "off=", $o, " len=", $l, " r=", count($r); + foreach ($r as $k => $v) { echo " ", $k, "=", $v; } + echo " a=", count($a); + foreach ($a as $k => $v) { echo " ", $k, "=", $v; } + echo "\n"; + } +} +"#, + ); + assert_eq!( + out, + r#"off=-10 len=omit r=4 0=1 1=2 2=3 3=4 a=0 +off=-10 len=null r=4 0=1 1=2 2=3 3=4 a=0 +off=-10 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=-10 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=-10 len=-3 r=1 0=1 a=3 0=2 1=3 2=4 +off=-10 len=-1 r=3 0=1 1=2 2=3 a=1 0=4 +off=-10 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=-10 len=1 r=1 0=1 a=3 0=2 1=3 2=4 +off=-10 len=3 r=3 0=1 1=2 2=3 a=1 0=4 +off=-10 len=4 r=4 0=1 1=2 2=3 3=4 a=0 +off=-10 len=10 r=4 0=1 1=2 2=3 3=4 a=0 +off=-4 len=omit r=4 0=1 1=2 2=3 3=4 a=0 +off=-4 len=null r=4 0=1 1=2 2=3 3=4 a=0 +off=-4 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=-4 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=-4 len=-3 r=1 0=1 a=3 0=2 1=3 2=4 +off=-4 len=-1 r=3 0=1 1=2 2=3 a=1 0=4 +off=-4 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=-4 len=1 r=1 0=1 a=3 0=2 1=3 2=4 +off=-4 len=3 r=3 0=1 1=2 2=3 a=1 0=4 +off=-4 len=4 r=4 0=1 1=2 2=3 3=4 a=0 +off=-4 len=10 r=4 0=1 1=2 2=3 3=4 a=0 +off=-3 len=omit r=3 0=2 1=3 2=4 a=1 0=1 +off=-3 len=null r=3 0=2 1=3 2=4 a=1 0=1 +off=-3 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=-3 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=-3 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=-3 len=-1 r=2 0=2 1=3 a=2 0=1 1=4 +off=-3 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=-3 len=1 r=1 0=2 a=3 0=1 1=3 2=4 +off=-3 len=3 r=3 0=2 1=3 2=4 a=1 0=1 +off=-3 len=4 r=3 0=2 1=3 2=4 a=1 0=1 +off=-3 len=10 r=3 0=2 1=3 2=4 a=1 0=1 +off=-1 len=omit r=1 0=4 a=3 0=1 1=2 2=3 +off=-1 len=null r=1 0=4 a=3 0=1 1=2 2=3 +off=-1 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=-1 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=-1 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=-1 len=-1 r=0 a=4 0=1 1=2 2=3 3=4 +off=-1 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=-1 len=1 r=1 0=4 a=3 0=1 1=2 2=3 +off=-1 len=3 r=1 0=4 a=3 0=1 1=2 2=3 +off=-1 len=4 r=1 0=4 a=3 0=1 1=2 2=3 +off=-1 len=10 r=1 0=4 a=3 0=1 1=2 2=3 +off=0 len=omit r=4 0=1 1=2 2=3 3=4 a=0 +off=0 len=null r=4 0=1 1=2 2=3 3=4 a=0 +off=0 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=0 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=0 len=-3 r=1 0=1 a=3 0=2 1=3 2=4 +off=0 len=-1 r=3 0=1 1=2 2=3 a=1 0=4 +off=0 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=0 len=1 r=1 0=1 a=3 0=2 1=3 2=4 +off=0 len=3 r=3 0=1 1=2 2=3 a=1 0=4 +off=0 len=4 r=4 0=1 1=2 2=3 3=4 a=0 +off=0 len=10 r=4 0=1 1=2 2=3 3=4 a=0 +off=1 len=omit r=3 0=2 1=3 2=4 a=1 0=1 +off=1 len=null r=3 0=2 1=3 2=4 a=1 0=1 +off=1 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=1 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=1 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=1 len=-1 r=2 0=2 1=3 a=2 0=1 1=4 +off=1 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=1 len=1 r=1 0=2 a=3 0=1 1=3 2=4 +off=1 len=3 r=3 0=2 1=3 2=4 a=1 0=1 +off=1 len=4 r=3 0=2 1=3 2=4 a=1 0=1 +off=1 len=10 r=3 0=2 1=3 2=4 a=1 0=1 +off=3 len=omit r=1 0=4 a=3 0=1 1=2 2=3 +off=3 len=null r=1 0=4 a=3 0=1 1=2 2=3 +off=3 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=3 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=3 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=3 len=-1 r=0 a=4 0=1 1=2 2=3 3=4 +off=3 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=3 len=1 r=1 0=4 a=3 0=1 1=2 2=3 +off=3 len=3 r=1 0=4 a=3 0=1 1=2 2=3 +off=3 len=4 r=1 0=4 a=3 0=1 1=2 2=3 +off=3 len=10 r=1 0=4 a=3 0=1 1=2 2=3 +off=4 len=omit r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=null r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=-1 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=1 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=3 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=4 r=0 a=4 0=1 1=2 2=3 3=4 +off=4 len=10 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=omit r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=null r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=-10 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=-4 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=-3 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=-1 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=0 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=1 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=3 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=4 r=0 a=4 0=1 1=2 2=3 3=4 +off=10 len=10 r=0 a=4 0=1 1=2 2=3 3=4 +"# + ); +} + +/// Regression: the refcounted slice/splice helpers apply the same window arithmetic. +/// +/// An `array>` source routes through `__rt_array_slice_refcounted` and +/// `__rt_array_splice_refcounted`, which retain each copied payload, so the negative-length clamp has +/// to hold there too or the retain loop runs off the end of the source payload. +#[test] +fn test_slice_splice_refcounted_offset_length_matrix_matches_php() { + let out = compile_and_run( + r#" [1, 2, 3, 4], "n" => 2, "z" => null]; +$a = array_slice($m["arr"], 0, -10); +echo "a=", count($a), "\n"; +$b = array_slice($m["arr"], 2, -1); +echo "b=", count($b); +foreach ($b as $v) { echo " ", $v; } +echo "\n"; +$c = array_slice($m["arr"], -3, $m["n"]); +echo "c=", count($c); +foreach ($c as $v) { echo " ", $v; } +echo "\n"; +$d = array_slice($m["arr"], 1, $m["z"]); +echo "d=", count($d); +foreach ($d as $v) { echo " ", $v; } +echo "\n"; +$opts = [null, -1, 2]; +$e = array_slice([1, 2, 3, 4], 0, $opts[$argc - 1]); +echo "e=", count($e); +foreach ($e as $v) { echo " ", $v; } +echo "\n"; +$f = array_slice([1, 2, 3, 4], 0, $opts[$argc]); +echo "f=", count($f); +foreach ($f as $v) { echo " ", $v; } +echo "\n"; +"#, + ); + assert_eq!( + out, + r#"a=0 +b=1 3 +c=2 2 3 +d=3 2 3 4 +e=4 1 2 3 4 +f=3 1 2 3 +"# + ); +} + +/// Regression: slicing an array of associative arrays received through an untyped parameter. +/// +/// The checker specializes `top($scores)` to `array>` from its only call site, +/// but EIR gives every undeclared parameter the boxed-`Mixed` ABI contract, so the slice helper +/// really produces an array of boxed cells. Taking the checker's narrower call-site type as the EIR +/// result layout made the backend reject the call outright ("array_slice result element PHP type +/// AssocArray { key: Str, value: Str } for source element PHP type Mixed"); reading each element as +/// a raw hash pointer instead would have been the silent version of the same bug. +#[test] +fn test_array_slice_of_assoc_rows_through_untyped_parameter_matches_php() { + let out = compile_and_run( + r#" $row) { + echo $k, "=", $row["name"], "|"; + } + echo "\n"; +} +top([["name" => "Ada"], ["name" => "Bob"], ["name" => "Cy"]]); +"#, + ); + assert_eq!( + out, + r#"1:Ada +2:Bob +0=Bob|1=Cy| +"# + ); +} + +/// Regression: the same boxed-`Mixed` receiver with scalar payloads, including negative lengths. +/// +/// `int` and `string` element types hit the same checker-versus-EIR disagreement as the associative +/// rows above, and the negative-`$length` rows keep the shared window arithmetic covered on the +/// boxed-`Mixed` lowering rather than only on the typed helpers. +#[test] +fn test_array_slice_of_scalars_through_untyped_parameter_matches_php() { + let out = compile_and_run( + r#" 1, 1 => 2, 3 => 3] → descending key order 3, 2, 1 with values 3, 1, 2. #[test] fn test_krsort() { let out = compile_and_run( r#" 1, 1 => 2, 3 => 3]; krsort($a); echo count($a); +foreach ($a as $k => $v) { echo ":", $k, "=", $v; } "#, ); - assert_eq!(out, "3"); + assert_eq!(out, "3:3=3:2=1:1=2"); } /// Verifies natsort sorts values naturally (human ordering), preserving key-value associations. diff --git a/tests/codegen/arrays/indexed/splice_replacement.rs b/tests/codegen/arrays/indexed/splice_replacement.rs new file mode 100644 index 0000000000..cd40e91c8e --- /dev/null +++ b/tests/codegen/arrays/indexed/splice_replacement.rs @@ -0,0 +1,337 @@ +//! Purpose: +//! PHP-differential regression tests for `array_splice()`'s fourth parameter, `$replacement`. +//! The AOT backend used to reject the argument outright (`array_splice() takes 2 or 3 +//! arguments`); these fixtures pin the window arithmetic, the receiver forms, the value +//! representations, and the heap balance of the insertion path. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected string in this file is verbatim `LC_ALL=C php` 8.4 output for the same fixture. +//! - The fixtures cover a replacement LONGER than the removed window (which forces +//! `__rt_array_grow` and therefore a receiver relocation), SHORTER than it, and a pure +//! insertion (`$length === 0`), plus negative `$offset`/`$length` combinations. +//! - The receiver matrix repeats the five forms that already worked with three arguments — +//! plain local, instance property, static property, array element, by-reference parameter — +//! because only the local form is written back by the backend directly; the other four are +//! rewritten into hidden temporaries by `ir_lower::expr::ref_place_args`, and the +//! by-reference parameter needs the receiver's ref cell republished after a relocation. +//! - The heap-debug fixtures assert `leak summary: clean` on the scalar and boxing insertion +//! paths, so an unreleased replacement literal or an unretained inserted payload fails here. + +use super::*; +use crate::support::compile_and_run_with_heap_debug; + +/// Verifies every `$replacement` window shape matches reference PHP: longer than the removed +/// span, shorter than it, a pure insertion, negative `$offset`, negative `$length`, a bare +/// scalar, `null`, an out-of-range offset on both ends, an omitted `$length`, and `[]`. +#[test] +fn test_array_splice_replacement_windows_match_php() { + let out = compile_and_run( + r#"items, 1, 2, [7,8,9]); echo implode(",", $r2), "|", implode(",", $b->items), "\n"; +$r3 = array_splice(Box::$st, 1, 2, [7,8,9]); echo implode(",", $r3), "|", implode(",", Box::$st), "\n"; +$nested = [[1,2,3,4]]; $r4 = array_splice($nested[0], 1, 2, [7,8,9]); echo implode(",", $r4), "|", implode(",", $nested[0]), "\n"; +$p = [1,2,3,4]; byref($p); echo implode(",", $p), "\n"; +$named = [1,2,3]; $r5 = array_splice($named, 1, replacement: [9]); echo implode(",",$r5), "|", implode(",", $named), "\n"; +$named2 = [1,2,3]; $r6 = array_splice(array: $named2, offset: 1, length: 1, replacement: [8,9]); echo implode(",",$r6), "|", implode(",", $named2), "\n"; +"#, + ); + assert_eq!( + out, + r#"2,3|1,7,8,9,4 +2,3|1,7,8,9,4 +2,3|1,7,8,9,4 +2,3|1,7,8,9,4 +2,3|1,7,8,9,4 +1,7,8,9,4 +2,3|1,9 +2|1,8,9,3 +"# + ); +} + +/// Verifies refcounted element payloads survive the insertion in both directions: strings and +/// integers spliced into a heterogeneous receiver, and nested arrays spliced into an array of +/// arrays. +/// +/// The heterogeneous receiver stores boxed Mixed cells, so a typed replacement such as +/// `["A","B","C"]` has to be boxed element by element; the array-of-arrays receiver takes the +/// replacement's payloads verbatim and has to retain each one. +#[test] +fn test_array_splice_replacement_refcounted_values_match_php() { + let out = compile_and_run( + r#"` slot would make `var_dump()` report `int(1)` where PHP reports `bool(true)`. +#[test] +fn test_array_splice_scalar_and_null_replacement_match_php() { + let out = compile_and_run( + r#"` at the IR level — `ichecked_add` boxes its result — +/// so the values have to be read back out of their Mixed cells before they land in an +/// `array` payload. Storing the cell pointers instead would print addresses. +#[test] +fn test_array_splice_boxed_arithmetic_replacement_matches_php() { + let out = compile_and_run_with_heap_debug( + r#"` and +/// `__rt_array_to_mixed` re-boxes the live payloads. Every one of these used to be a hard +/// `unsupported EIR backend feature: array_splice replacement PHP type …` compile error. +#[test] +fn test_array_splice_type_changing_replacement_matches_php() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, "\n"; } +"#, + ); + assert_eq!( + out, + r#"Array +( + [0] => 1 + [1] => x + [2] => y + [3] => z + [4] => 4 + [5] => 5 +) +string(1) "x" +6 +0=>1 +1=>x +2=>y +3=>z +4=>4 +5=>5 +"# + ); +} + +/// Pins the DOCUMENTED refusal: a type-changing `$replacement` on a receiver whose storage this +/// call cannot retype stays a named compile error rather than becoming a wrong answer. +/// +/// A by-reference parameter shares its storage with a caller slot the callee cannot widen, so +/// promoting it would publish boxed `Mixed` cells through a slot the caller still reads as +/// `array`. The diagnostic has to say so, because "use a local" is the actual workaround. +#[test] +fn test_array_splice_type_changing_replacement_on_by_ref_parameter_is_refused() { + let error = crate::support::compile_source_expect_backend_error( + r#"`. +//! The three-argument form used to corrupt memory: the splice helpers built the removed-elements +//! array with `__rt_array_new(n, 8)` and moved 8-byte slots, while indexed string arrays store +//! 16-byte `{pointer, length}` pairs. `array_splice(["a","b","c","d"], 1, 2)` therefore answered +//! `[1, 4362860248]` — a raw heap pointer surfacing as a PHP integer — and left the receiver +//! half-shifted. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected string in this file is verbatim `LC_ALL=C php` 8.4 output for the same fixture. +//! - `__rt_array_splice_str` MOVES the removed string payloads into the result: an indexed string +//! array owns its persisted bytes exclusively, so the heap-debug fixtures below would report a +//! double free (a crash) if it retained them and a leak if it copied them. +//! - `__rt_array_splice_insert_str` DUPLICATES each inserted replacement string, because the +//! replacement array keeps owning its own payloads and the caller releases it afterwards. +//! - The long-replacement fixtures force `__rt_array_grow`, which relocates the receiver, so they +//! also cover the write-back of the new pointer. + +use super::*; +use crate::support::compile_and_run_with_heap_debug; + +/// Verifies the three-argument removal on a string receiver: the removed elements come back as +/// strings and the receiver keeps exactly the surviving ones, in order. +/// +/// This is the fixture whose second element used to print a raw pointer. +#[test] +fn test_array_splice_string_removal_matches_php() { + let out = compile_and_run( + r#"items, 1, 1, ["Z"]); echo implode(",",$s->items), "\n"; +array_splice(S::$shared, 1, 1, ["Z"]); echo implode(",",S::$shared), "\n"; + +$nested = [["a","b","c"]]; array_splice($nested[0], 1, 1, ["Z"]); echo implode(",",$nested[0]), "\n"; +"#, + ); + assert_eq!( + out, + r#"a,Z,c|a,b,c +a,Z,c +a,Z,c +a,Z,c +a,Z,c +"# + ); +} + +/// Verifies the string splice keeps the heap balanced in a loop that both removes and inserts. +/// +/// The removed payloads are handed to the result array (which is released at the end of each +/// iteration) and the inserted ones are freshly persisted, so a retained-instead-of-moved +/// removal would double free and a copied-instead-of-moved removal would leak one block per +/// element. +#[test] +fn test_array_splice_string_insertion_leaves_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#" 1, "y" => 2, "z" => 3]; +echo current($a), key($a), next($a), key($a), end($a), key($a), reset($a), key($a); +var_dump(prev($a)); var_dump(key($a)); +"#, + ); + assert_eq!(out, "1x2y3z1xbool(false)\nNULL\n"); +} + +/// Verifies every family member reports the empty position on an empty array. +/// Fixture: `[]`, where `key()` yields null and the other five yield false. +#[test] +fn test_array_internal_pointer_empty_array() { + let out = compile_and_run( + r#"", $v, " "; next($w); } +echo "|"; +$h = ["a"=>1,"b"=>2,"c"=>3]; +reset($h); +while (($v = current($h)) !== false) { echo key($h), "=>", $v, " "; next($h); } +"#, + ); + assert_eq!(out, "0=>1 1=>2 2=>3 3=>4 |a=>1 b=>2 c=>3 "); +} + +/// Verifies `foreach` does not move the internal pointer, by value or by reference. +/// PHP 7+ iterates an internal copy, so the pointer must sit where `next()` left it. +/// Fixture: pointer advanced to key 1, then two full `foreach` passes. +#[test] +fn test_array_internal_pointer_foreach_does_not_move_it() { + let out = compile_and_run( + r#" $vv) {} +var_dump(key($f)); +foreach ($f as &$r) {} +unset($r); +var_dump(key($f)); +"#, + ); + assert_eq!(out, "int(1)\nint(1)\n"); +} + +/// Verifies binding the variable to a different array rewinds its pointer, because PHP's +/// pointer belongs to the hashtable rather than to the variable. +/// Fixture: advance the pointer, then assign a fresh array over the same local. +#[test] +fn test_array_internal_pointer_reassignment_rewinds() { + let out = compile_and_run( + r#" "a", 9 => "b"]; +var_dump(key($a)); next($a); var_dump(key($a)); +"#, + ); + assert_eq!(out, "int(5)\nint(9)\n"); +} + +/// Verifies the family resolves case-insensitively and through a root-namespace prefix, +/// like every other PHP-visible builtin. +/// Fixture: `CURRENT`, `\key`, `Next`, `RESET` and `End` on one array. +#[test] +fn test_array_internal_pointer_case_insensitive_and_namespaced() { + let out = compile_and_run( + r#"1,"y"=>2]; echo current($h), key($h); next($h); echo current($h), key($h);'); +"#, + ); + assert_eq!(out, "10|0|20|1|30|2|bool(false)\nNULL\n10|0\n1x2y"); +} diff --git a/tests/codegen/arrays/key_sort.rs b/tests/codegen/arrays/key_sort.rs new file mode 100644 index 0000000000..af3e179ad7 --- /dev/null +++ b/tests/codegen/arrays/key_sort.rs @@ -0,0 +1,313 @@ +//! Purpose: +//! Regression tests for the associative-array sorts that reorder a hash table's +//! insertion-order chain: `ksort()`, `krsort()`, `asort()` and `arsort()`. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expectation is verbatim `LC_ALL=C php` (PHP 8.4.20) output for the same fixture. +//! - Before this suite, `ksort()`/`krsort()` on a hash were runtime no-ops that returned +//! the receiver untouched with no diagnostic; the string-key case is the original repro. +//! - Sorting only relinks `prev`/`next`/`head`/`tail`, so the fixtures also assert that key +//! association, later key lookups, later inserts and copy-on-write all still hold, and +//! one fixture re-checks the heap under `--heap-debug`. +//! - PHP's key ordering is `zend_compare`, not a byte-wise order: `10` sorts before +//! `'Banana'` and `'0.5'` before `2`, which the mixed-key fixture pins. + +use crate::support::*; + +/// Issue repro: `ksort()`/`krsort()` over a string-keyed associative array used to leave the +/// receiver in insertion order without any diagnostic. Both directions must now reorder it. +#[test] +fn test_ksort_krsort_string_keys() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1]; +ksort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +echo "|"; +krsort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "a=3;b=2;c=1;|c=1;b=2;a=3;"); +} + +/// The original one-line repro: `implode(",", array_keys($a))` after `ksort()`. +#[test] +fn test_ksort_string_keys_through_array_keys() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1]; +ksort($a); +echo implode(",", array_keys($a)); +"#, + ); + assert_eq!(out, "a,b,c"); +} + +/// Sparse integer keys must sort numerically (`-1 < 2 < 10 < 33`), not by insertion order +/// and not by the decimal text of the key. +#[test] +fn test_ksort_krsort_integer_keys() { + let out = compile_and_run( + r#" "x", 2 => "y", 33 => "z", -1 => "w"]; +ksort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +echo "|"; +krsort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "-1=w;2=y;10=x;33=z;|33=z;10=x;2=y;-1=w;"); +} + +/// Mixed integer and string keys follow PHP's standard comparison, so `''` sorts before the +/// integer `2` and the integer `10` sorts before `'Banana'` — not a lexicographic order. +#[test] +fn test_ksort_krsort_mixed_int_and_string_keys() { + let out = compile_and_run( + r#" "a", "9" => "b", "apple" => "c", "Banana" => "d", 2 => "e", "" => "f"]; +ksort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +echo "|"; +$b = [10 => "a", "9" => "b", "apple" => "c", "Banana" => "d", 2 => "e", "" => "f"]; +krsort($b); +foreach ($b as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!( + out, + "=f;2=e;9=b;10=a;Banana=d;apple=c;|apple=c;Banana=d;10=a;9=b;2=e;=f;" + ); +} + +/// An empty receiver — both a literal `[]` and a hash emptied with `unset()` — must sort to +/// itself without touching the header's head/tail sentinels. +#[test] +fn test_ksort_krsort_empty_array() { + let out = compile_and_run( + r#" 1]; +unset($b["k"]); +ksort($b); +echo count($b), ";"; +krsort($b); +echo count($b); +"#, + ); + assert_eq!(out, "0;0;0;0"); +} + +/// A single-entry hash must survive both directions with its one key/value pair intact. +#[test] +fn test_ksort_krsort_single_element() { + let out = compile_and_run( + r#" 7]; +ksort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +krsort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "only=7;only=7;"); +} + +/// `asort()`/`arsort()` over duplicate values must be stable in both directions: `b`, `d` +/// and `e` all hold `2` and keep their original relative order, exactly like PHP 8. +#[test] +fn test_asort_arsort_duplicate_values_are_stable() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1, "d" => 2, "e" => 2]; +asort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +echo "|"; +$b = ["b" => 2, "a" => 3, "c" => 1, "d" => 2, "e" => 2]; +arsort($b); +foreach ($b as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "c=1;b=2;d=2;e=2;a=3;|a=3;b=2;d=2;e=2;c=1;"); +} + +/// `asort()`/`arsort()` over string values compare with PHP's ordering, not by slot width. +#[test] +fn test_asort_arsort_string_values() { + let out = compile_and_run( + r#" "pear", "a" => "apple", "c" => "fig"]; +asort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +echo "|"; +arsort($a); +foreach ($a as $k => $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "a=apple;c=fig;b=pear;|b=pear;c=fig;a=apple;"); +} + +/// Copy-on-write: a copy taken before the sort must keep the original iteration order, in +/// both directions. The sorters mutate the table in place, so the receiver has to be split +/// with `__rt_hash_ensure_unique` first. +#[test] +fn test_ksort_krsort_does_not_mutate_aliased_copy() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1]; +$copy = $a; +ksort($a); +foreach ($a as $k => $v) { echo $k; } +echo "|"; +foreach ($copy as $k => $v) { echo $k; } +echo "|"; +$other = $a; +krsort($a); +foreach ($a as $k => $v) { echo $k; } +echo "|"; +foreach ($other as $k => $v) { echo $k; } +"#, + ); + assert_eq!(out, "abc|bac|cba|abc"); +} + +/// Sorting must not disturb the hash's probe layout: key lookups, a later insert, and the +/// live count all still work on the reordered table. +#[test] +fn test_ksort_preserves_lookup_and_later_inserts() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1]; +ksort($a); +echo $a["a"], $a["b"], $a["c"], ";"; +$a["d"] = 4; +foreach ($a as $k => $v) { echo $k, $v; } +echo ";", count($a); +"#, + ); + assert_eq!(out, "321;a3b2c1d4;4"); +} + +/// Repeated sorts in both directions must keep converging on the same orders instead of +/// corrupting the insertion-order chain after the first relink. +#[test] +fn test_repeated_key_and_value_sorts_stay_consistent() { + let out = compile_and_run( + r#" 2, "a" => 3, "c" => 1, "d" => 4, "e" => 5]; +krsort($a); +foreach ($a as $k => $v) { echo $k; } +echo "|"; +ksort($a); +foreach ($a as $k => $v) { echo $k; } +echo "|"; +asort($a); +foreach ($a as $k => $v) { echo $k; } +"#, + ); + assert_eq!(out, "edcba|abcde|cbade"); +} + +/// An input that is already in the requested order must come back unchanged, which also +/// exercises the backward scan's immediate-stop path. +#[test] +fn test_key_sorts_on_already_ordered_input() { + let out = compile_and_run( + r#" 1, "b" => 2, "c" => 3]; +ksort($a); +foreach ($a as $k => $v) { echo $k; } +echo "|"; +$b = ["c" => 3, "b" => 2, "a" => 1]; +krsort($b); +foreach ($b as $k => $v) { echo $k; } +"#, + ); + assert_eq!(out, "abc|cba"); +} + +/// `ksort()` on an indexed array stays a no-op: its keys are the slot positions `0..n-1`, +/// which are already in ascending key order, and the values keep their slots. +#[test] +fn test_ksort_on_indexed_array_is_a_noop() { + let out = compile_and_run( + r#" $v) { echo $k, "=", $v, ";"; } +"#, + ); + assert_eq!(out, "0=3;1=1;2=2;"); +} + +/// `krsort()` on a non-empty indexed array must be refused by name rather than silently +/// returning the receiver untouched: indexed storage has no room for a descending key order. +#[test] +fn test_krsort_on_indexed_array_reports_named_backend_error() { + let error = compile_source_expect_backend_error( + r#""), + "unexpected diagnostic: {error}" + ); + assert!( + error.contains("descending key order has no representation"), + "unexpected diagnostic: {error}" + ); +} + +/// `krsort()` on a statically empty indexed array stays accepted, because an empty receiver +/// is trivially representable in either direction. +#[test] +fn test_krsort_on_empty_indexed_array_is_accepted() { + let out = compile_and_run( + r#" "two", "aa" => "three", "cc" => "one"]; +$b = $a; +ksort($a); +krsort($b); +asort($a); +arsort($b); +foreach ($a as $k => $v) { echo $k, $v; } +foreach ($b as $k => $v) { echo $k, $v; } +"#, + ); + assert_eq!( + out.stdout, "cconeaathreebbtwobbtwoaathreeccone", + "stderr: {}", + out.stderr + ); + assert!( + out.stderr.contains("HEAP DEBUG: leak summary: clean"), + "expected clean heap, got: {}", + out.stderr + ); +} diff --git a/tests/codegen/arrays/list_unpack.rs b/tests/codegen/arrays/list_unpack.rs index 8b91041ff6..1fd5562849 100644 --- a/tests/codegen/arrays/list_unpack.rs +++ b/tests/codegen/arrays/list_unpack.rs @@ -71,3 +71,51 @@ echo $left . ":" . $right; ); assert_eq!(out, "left:right"); } + +/// Verifies `foreach` value destructuring in both spellings PHP accepts (`[...]` and +/// `list(...)`), plus the `$key => [...]` form. Expected output matches `php -r` on 8.4. +#[test] +fn test_foreach_value_destructuring() { + let out = compile_and_run( + r#" [$a, $b]) { echo $k, ":", $a, ",", $b, ";"; } +"#, + ); + assert_eq!(out, "1-2;3-4;1+2;3+4;0:1,2;1:3,4;"); +} + +/// Verifies keyed, skipped-element, and nested `foreach` destructuring patterns, which all +/// reuse the same lowering as a standalone `[...] = $value;` assignment. +#[test] +fn test_foreach_destructuring_keyed_skipped_and_nested() { + let out = compile_and_run( + r#" "ann", "age" => 30], ["name" => "bob", "age" => 40]]; +foreach ($pairs as ["name" => $n, "age" => $g]) { echo $n, "=", $g, ";"; } +$skip = [[1, 2, 3], [4, 5, 6]]; +foreach ($skip as [, $second]) { echo $second, ";"; } +$nested = [[1, [2, 3]], [4, [5, 6]]]; +foreach ($nested as [$x, [$y, $z]]) { echo $x, $y, $z, ";"; } +"#, + ); + assert_eq!(out, "ann=30;bob=40;2;5;123;456;"); +} + +/// Verifies destructuring `foreach` loops nest, and that the pattern also works with a +/// single-statement body and inside a function over an `array`-hinted parameter. +#[test] +fn test_foreach_destructuring_nested_loops_and_bodies() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } +} +echo "after\n"; +"#, + ); + assert_eq!( + out, + format!("{FILL_COUNT_TOO_LARGE}\n").repeat(5) + FILL_COUNT_NEGATIVE + "\nafter\n" + ); +} + +/// Regression: the keyed and string fill helpers are guarded by the same bound, so a non-zero +/// `$start` (which routes the count into `__rt_hash_new` as a bucket capacity) and a string value +/// (whose helper takes `$count` in the first ABI argument register) both throw as well. +#[test] +fn test_array_fill_oversized_count_throws_on_every_fill_helper() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } + try { + $s = array_fill(0, $n, "x"); + echo "no throw ", count($s), "\n"; + } catch (ValueError $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } +} +echo "after\n"; +"#, + ); + assert_eq!(out, format!("{FILL_COUNT_TOO_LARGE}\n").repeat(4) + "after\n"); +} + +/// Verifies an uncaught oversized `$count` terminates with the PHP-shaped uncaught diagnostic +/// naming `ValueError`, not with the allocator's heap fatal. +#[test] +fn test_array_fill_oversized_count_uncaught_is_fatal() { + let err = + compile_and_run_expect_failure("getMessage(), "\n"; } +try { $b = range(3000000000, 1); echo "no throw\n"; } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +try { $c = range(0, 1073741823); echo "no throw\n"; } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +try { $d = range(0, 4294967292, 4); echo "no throw\n"; } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +try { $f = range(3000000000, 1, -2); echo "no throw\n"; } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +try { $g = range(PHP_INT_MIN, PHP_INT_MAX, 2); echo "no throw\n"; } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +echo "after\n"; +"#, + ); + assert_eq!( + out, + r#"ValueError: The supplied range exceeds the maximum array size: start=1 end=3000000000 step=1 +ValueError: The supplied range exceeds the maximum array size: start=1 end=3000000000 step=1 +ValueError: The supplied range exceeds the maximum array size: start=0 end=1073741823 step=1 +ValueError: The supplied range exceeds the maximum array size: start=0 end=4294967292 step=4 +ValueError: The supplied range exceeds the maximum array size: start=1 end=3000000000 step=2 +ValueError: The supplied range exceeds the maximum array size: start=-9223372036854775808 end=9223372036854775807 step=2 +after +"# + ); +} + +/// Verifies an uncaught oversized `range()` terminates with the PHP-shaped uncaught diagnostic +/// naming `ValueError` and carrying the interpolated interval. +/// +/// The message is built at runtime, so this also pins the dynamic-message throw path: before it +/// existed, a throwable whose text is only known at runtime reported the unwinder's generic +/// `uncaught exception` line instead of naming its class. +#[test] +fn test_range_oversized_span_uncaught_is_fatal() { + let err = compile_and_run_expect_failure("getMessage(), "\n"; } +try { range(1, 3000000000, $n); echo "no throw\n"; } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +try { range(1, 3, $w); echo "no throw\n"; } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +echo implode(",", range(1, 9, 2)), "|", implode(",", range(5, 1, -2)), "|", implode(",", range(7, 7, 100)), "\n"; +"#, + ); + assert_eq!( + out, + r#"range(): Argument #3 ($step) cannot be 0 +range(): Argument #3 ($step) must be greater than 0 for increasing ranges +range(): Argument #3 ($step) must be less than the range spanned by argument #1 ($start) and argument #2 ($end) +1,3,5,7,9|5,3,1|7 +"# + ); +} + +/// Regression: the widest possible interval spans `2^64 - 1`, which every step magnitude fits, so +/// PHP rejects it for its SIZE and not for its step. +/// +/// The step-magnitude guard used to take a signed absolute of `end - start`, which wraps to `1` +/// for `PHP_INT_MIN`..`PHP_INT_MAX` and made every step past `1` look wider than the interval. +/// php-src reads that subtraction as unsigned, so the guard now orders the endpoints first and +/// compares against the unsigned width. +#[test] +fn test_range_widest_interval_reports_the_size_error_not_the_step_error() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +} +"#, + ); + assert_eq!( + out, + r#"The supplied range exceeds the maximum array size: start=-9223372036854775808 end=9223372036854775807 step=2 +The supplied range exceeds the maximum array size: start=-9223372036854775808 end=9223372036854775807 step=4294967296 +"# + ); +} + +/// Positive control: ordinary fills and ranges — indexed, keyed, string-valued, ascending, +/// descending, stepped and degenerate — keep producing exactly what reference PHP produces. +#[test] +fn test_ordinary_fills_and_ranges_still_work() { + let out = compile_and_run( + r#" $v) { echo "$k=$v,"; } +echo "|", implode(",", range(1, 5)), "|", implode(",", range(5, 1)), "|", implode(",", range(-3, 3, 2)), "|", implode(",", range(1073741823, 1073741823)), "\n"; +"#, + ); + assert_eq!( + out, + "4:7,7,7,7|3:x,x,x|5=1,6=1,7=1,|1,2,3,4,5|5,4,3,2,1|-3,-1,1,3|1073741823\n" + ); +} + +/// Regression guard for the accepted side of the `range()` boundary and for the clean +/// heap-exhaustion path: `range(-1073741822, 0)` asks for exactly `1073741823` elements, the most +/// reference PHP will build, so the size guard must let it through to the allocator — which then +/// reports heap exhaustion, the condition PHP has no `ValueError` for. +/// +/// One element more (`range(-1073741823, 0)`) is the `ValueError` asserted above, so the pair pins +/// the boundary from both sides without ever completing an 8 GiB allocation. +#[test] +fn test_range_largest_accepted_span_still_reaches_heap_exhaustion() { + let err = compile_and_run_expect_failure(" $v)`, `count()` and key lookups rather than `implode()`, which does not +//! accept an associative array in elephc. +//! - The dynamic-callable fixtures pin the wrapper ABI: the shape-changing `preserve_keys` flag is +//! dropped from the callable signature, and the typed runtime target must still supply a +//! concrete container layout where no per-call-site checked type exists. + +use crate::support::*; + +/// Verifies PHP's one-argument `implode($array)` form joins with an empty separator. +#[test] +fn test_implode_single_array_argument() { + let out = compile_and_run(" 1, "b" => 2]; echo var_export(array_search(2, $m, true), true), ":", var_export(array_search(2, $m, false), true);"#, + ); + assert_eq!(out, "'b':'b'"); +} + +/// Verifies a runtime-unknown `strict` flag selects the mode at run time. +#[test] +fn test_array_search_strict_runtime_flag() { + let out = compile_and_run( + r#" 0; echo var_export(array_search(20, $a, $t), true);"#, + ); + assert_eq!(out, "1"); +} + +/// Verifies strict search over a string haystack keeps exact comparison. +#[test] +fn test_array_search_strict_string_haystack() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "2=>3 1=>2 0=>1 "); +} + +/// Verifies the `preserve_keys:` named argument and the resulting key-addressable lookups. +#[test] +fn test_array_reverse_preserve_keys_named() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; } echo "|", $x[0], ",", $x[2];"#, + ); + assert_eq!(out, "2=>30 1=>20 0=>10 |10,30"); +} + +/// Verifies an explicit `false` flag keeps the renumbered indexed-array result. +#[test] +fn test_array_reverse_preserve_keys_false() { + let out = compile_and_run(r#" $v) { echo $k, "=>", $v, " "; } echo "|", implode(",", $s);"#, + ); + assert_eq!(out, "2=>r 1=>q 0=>p |p,q,r"); +} + +/// Verifies the positional `range($start, $end, $step)` form for ascending ranges. +#[test] +fn test_range_step_positional() { + let out = compile_and_run( + r#"getMessage(); }"#, + ); + assert_eq!(out, "ValueError: range(): Argument #3 ($step) cannot be 0"); +} + +/// Verifies a negative `range()` step on an increasing range raises PHP's `ValueError`. +#[test] +fn test_range_step_negative_value_error() { + let out = compile_and_run( + r#"getMessage(); }"#, + ); + assert_eq!( + out, + "ValueError: range(): Argument #3 ($step) must be greater than 0 for increasing ranges" + ); +} + +/// Verifies a `range()` step wider than the spanned interval raises PHP's `ValueError`. +#[test] +fn test_range_step_too_wide_value_error() { + let out = compile_and_run( + r#"getMessage(); }"#, + ); + assert_eq!( + out, + "ValueError: range(): Argument #3 ($step) must be less than the range spanned by argument #1 ($start) and argument #2 ($end)" + ); +} + +/// Verifies `array_slice($array, $offset, $length, true)` keeps the source integer keys. +#[test] +fn test_array_slice_preserve_keys_positional() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "1=>20 2=>30 3=>40 "); +} + +/// Verifies the fully named `array_slice()` call form, including `preserve_keys:`. +#[test] +fn test_array_slice_preserve_keys_named() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "1=>20 2=>30 "); +} + +/// Verifies a `preserve_keys:` named argument that skips the optional `$length` slot. +/// +/// The argument planner has to fill the gap with `$length`'s `null` default, so the runtime +/// helper still receives its four-value argument tuple and takes every remaining element. +#[test] +fn test_array_slice_preserve_keys_named_skipping_length() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "1=>20 2=>30 3=>40 4=>50 "); +} + +/// Verifies a negative `array_slice()` offset with an explicit `null` length keeps the tail keys. +#[test] +fn test_array_slice_preserve_keys_negative_offset() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "3=>40 4=>50 "); +} + +/// Verifies a negative `array_slice()` length stops before the end while keeping the keys. +/// +/// This pins the shared `emit_slice_bounds` window arithmetic on the key-preserving helper: a +/// negative length counts back from the SOURCE end, not from the offset. +#[test] +fn test_array_slice_preserve_keys_negative_length() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "1=>20 2=>30 3=>40 "); +} + +/// Verifies the key-preserving slice clamps out-of-range windows to an empty result. +/// +/// A backward length that consumes more than the window holds, and an offset past the end, must +/// both yield zero elements rather than a negative window that would read outside the payload. +#[test] +fn test_array_slice_preserve_keys_empty_windows() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "0=>10 1=>20 "); +} + +/// Verifies an explicit `false` flag keeps the renumbered indexed-array result. +#[test] +fn test_array_slice_preserve_keys_false() { + let out = compile_and_run( + r#" $v) { echo $k, "=>", $v, " "; } echo "|"; $f = [1.5, 2.5, 3.5, 4.5]; $z = array_slice($f, 2, 2, true); foreach ($z as $k => $v) { echo $k, "=>", $v, " "; }"#, + ); + assert_eq!(out, "1=>two 2=>3.5 |2=>3.5 3=>4.5 "); +} + +/// Verifies dynamic `array_slice` dispatch still produces a concrete indexed array. +/// +/// Both callable spellings drop the shape-changing `$preserve_keys` flag from the wrapper ABI, +/// so they must keep working exactly like the three-argument direct call. +#[test] +fn test_array_slice_dynamic_callable_dispatch() { + let out = compile_and_run( + r#" $chunk) { echo "[", $ci, "]"; foreach ($chunk as $k => $v) { echo " ", $k, "=>", $v; } echo "|"; }"#, + ); + assert_eq!(out, "[0] 0=>10 1=>20|[1] 2=>30 3=>40|[2] 4=>50|"); +} + +/// Verifies the `preserve_keys:` named argument on an unevenly divided source. +#[test] +fn test_array_chunk_preserve_keys_named() { + let out = compile_and_run( + r#" $chunk) { echo "[", $ci, "]"; foreach ($chunk as $k => $v) { echo " ", $k, "=>", $v; } echo "|"; }"#, + ); + assert_eq!(out, "[0] 0=>10 1=>20 2=>30|[1] 3=>40 4=>50|"); +} + +/// Verifies a fully named `array_chunk()` call whose chunk size exceeds the source length. +#[test] +fn test_array_chunk_preserve_keys_all_named_oversized_length() { + let out = compile_and_run( + r#" $chunk) { echo "[", $ci, "]"; foreach ($chunk as $k => $v) { echo " ", $k, "=>", $v; } echo "|"; }"#, + ); + assert_eq!(out, "[0] 0=>10 1=>20 2=>30 3=>40 4=>50|"); +} + +/// Verifies key-preserving chunks stay countable and key-addressable from the outer array. +#[test] +fn test_array_chunk_preserve_keys_nested_lookup() { + let out = compile_and_run( + r#" $chunk) { echo "[", $ci, "]"; foreach ($chunk as $k => $v) { echo " ", $k, "=>", $v; } echo "|"; }"#, + ); + assert_eq!(out, "[0] 0=>1 1=>two|[1] 2=>3.5 3=>1|"); +} + +/// Verifies an explicit `false` flag keeps the renumbered nested indexed arrays. +#[test] +fn test_array_chunk_preserve_keys_false() { + let out = compile_and_run( + r#" $chunk) { echo "[", $ci, "]"; foreach ($chunk as $k => $v) { echo " ", $k, "=>", $v; } echo "|"; }"#, + ); + assert_eq!(out, "[0] 0=>10 1=>20|[1] 0=>30 1=>40|[2] 0=>50|"); +} + +/// Verifies the key-preserving chunk form still raises PHP's non-positive `$length` `ValueError`. +#[test] +fn test_array_chunk_preserve_keys_zero_length_value_error() { + let out = compile_and_run( + r#"getMessage(); }"#, + ); + assert_eq!( + out, + "ValueError: array_chunk(): Argument #2 ($length) must be greater than 0" + ); +} + +/// Verifies dynamic `array_chunk` dispatch produces the renumbered nested arrays. +/// +/// The callable ABI drops the shape-changing `$preserve_keys` flag, and the typed runtime target +/// supplies the concrete `array>` layout that a wrapper has no checked type for. +#[test] +fn test_array_chunk_dynamic_callable_dispatch() { + let out = compile_and_run( + r#"()` length whose `len * 8` payload size wraps the machine word +/// is rejected. Before the guard the wrapped product allocated 32 bytes while the header still +/// advertised the pre-overflow length, so `$b[0x100000]` passed the bounds check and read roughly +/// 8 MB past the block. +#[test] +fn test_buffer_new_overflowing_length_is_fatal() { + let err = compile_and_run_expect_failure( + r#" $b = buffer_new(0x2000000000000002); +echo $b[0]; +"#, + ); + assert!( + err.contains("buffer_new() length is negative or exceeds the maximum buffer size"), + "{}", + err + ); +} + +/// Verifies that an out-of-range index cannot follow an overflowing `buffer_new()`: the +/// allocation itself aborts, so the read never reaches the payload. Before the guard this program +/// printed `read:0` after loading roughly 8 MB past the 32-byte allocation and exited 0. +#[test] +fn test_buffer_new_overflow_prevents_out_of_range_read() { + let out = compile_and_run_capture( + r#" $b = buffer_new(0x2000000000000002); +echo "read:", $b[0x100000]; +"#, + ); + assert!(!out.success, "overflowing buffer_new unexpectedly succeeded"); + assert!( + out.stderr + .contains("buffer_new() length is negative or exceeds the maximum buffer size"), + "{}", + out.stderr + ); + assert_eq!(out.stdout, ""); +} + +/// Verifies that an out-of-range write cannot follow an overflowing `buffer_new()` either. +#[test] +fn test_buffer_new_overflow_prevents_out_of_range_write() { + let err = compile_and_run_expect_failure( + r#" $b = buffer_new(0x2000000000000002); +$b[0x40000000] = 1; +echo "wrote"; +"#, + ); + assert!( + err.contains("buffer_new() length is negative or exceeds the maximum buffer size"), + "{}", + err + ); +} + +/// Verifies that a negative `buffer_new()` length is rejected with the same controlled fatal +/// instead of being multiplied as an unsigned value into a huge allocation request. +#[test] +fn test_buffer_new_negative_length_is_fatal() { + let err = compile_and_run_expect_failure( + r#" $b = buffer_new(-2); +echo buffer_len($b); +"#, + ); + assert!( + err.contains("buffer_new() length is negative or exceeds the maximum buffer size"), + "{}", + err + ); +} + +/// Positive control: an ordinary small `buffer_new()` still allocates, zero-initializes, +/// reads, and writes, so the length guard did not narrow the common path. +#[test] +fn test_buffer_new_normal_length_still_works() { + let out = compile_and_run( + r#" $b = buffer_new(4); +$b[2] = 42; +echo buffer_len($b), ":", $b[0], ":", $b[2]; +"#, + ); + assert_eq!(out, "4:0:42"); +} + +/// Verifies the linux-x86_64 runtime carries the same `__rt_buffer_new` length guard as the ARM64 +/// runtime. The x86_64 code cannot be executed from an aarch64 host, so this asserts on the emitted +/// assembly text. +#[test] +fn test_x86_64_runtime_buffer_new_carries_length_guard() { + let target = Target::parse("linux-x86_64").expect("linux-x86_64 is a supported target"); + let runtime_asm = elephc::codegen::generate_runtime(8_388_608, target); + let marker = "__rt_buffer_new:"; + let start = runtime_asm + .find(marker) + .expect("missing assembly label __rt_buffer_new"); + let rest = &runtime_asm[start..]; + let buffer_new = &rest[..rest.find("\n\n").unwrap_or(rest.len())]; + for expected in [ + "js __rt_buffer_new_size_fail", + "imul rax, rdi", + "jo __rt_buffer_new_size_fail", + "add rax, 16", + ] { + assert!( + buffer_new.contains(expected), + "x86_64 __rt_buffer_new missing {expected}: {buffer_new}" + ); + } + assert!( + runtime_asm.contains("__rt_buffer_new_size_fail:"), + "x86_64 runtime is missing the buffer-length fatal handler" + ); +} diff --git a/tests/codegen/callables/callable_strings.rs b/tests/codegen/callables/callable_strings.rs new file mode 100644 index 0000000000..0d5b559977 --- /dev/null +++ b/tests/codegen/callables/callable_strings.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! End-to-end coverage for PHP callable strings bound to a declared `callable` parameter +//! (`function apply(callable $f) {...} apply("strtoupper", ...)`), covering plain function +//! names, `"Class::method"` names, case-insensitive and namespaced spellings, and the named +//! argument form. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected value is verbatim `LC_ALL=C php` 8.4.20 stdout. +//! - elephc resolves callables statically, so only a compile-time-known string binds; the +//! rejected shapes are pinned in `tests/error_tests/callables.rs`. + +use crate::support::*; + +/// Verifies a builtin function-name string binds to a declared `callable` parameter and is +/// invoked inside the callee — the repro from the parameter-typing audit. +#[test] +fn test_builtin_name_string_binds_to_callable_parameter() { + let out = compile_and_run( + r#""; } + } + function apply(callable $f, string $s) { return $f($s); } + echo apply("Formatter::wrap", "abc"); + "#, + ); + assert_eq!(out, ""); +} + +/// Verifies the binding also fires when the callable string is passed as a named argument, +/// which reaches EIR through the reordered named-argument path. +#[test] +fn test_callable_name_string_binds_through_named_argument() { + let out = compile_and_run( + r#"run("strtoupper", "abc"); + "#, + ); + assert_eq!(out, "ABC"); +} + +/// Verifies a bound callable string carries its signature into the callee, so a call with the +/// wrong argument count is still rejected rather than silently accepted. +#[test] +fn test_bound_callable_string_keeps_working_alongside_first_class_callables() { + let out = compile_and_run( + r#" $x . "!", "e"); + "#, + ); + assert_eq!(out, "ABcde!"); +} diff --git a/tests/codegen/callables/closure_array_returns.rs b/tests/codegen/callables/closure_array_returns.rs new file mode 100644 index 0000000000..b4daeb836d --- /dev/null +++ b/tests/codegen/callables/closure_array_returns.rs @@ -0,0 +1,201 @@ +//! Purpose: +//! Regression tests for closures and arrow functions that return an array literal built +//! directly out of their own parameters or captured variables. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected value is verbatim `LC_ALL=C php` output from PHP 8.4.20. +//! - A closure with no declared return type infers one from a single `return ;` body +//! (`direct_closure_return_type` in `src/ir_lower/function.rs`). That inference used to fall +//! back to the syntactic `int` default for an array literal, so +//! `function (mixed $a, mixed $b) { return [$a, $b]; }` was stamped `array` and +//! `$f(1, "z")` returned `[1, 0]` — the boxed `Mixed` argument was cast to an integer on the +//! way into the array. Typed `string`/`float`/`bool`/`array` parameters were mis-stamped the +//! same way. +//! - The shapes that always worked (named function, method, literal assigned to a local before +//! returning, explicit `: array`) are pinned here too so the fix keeps them working. +//! - `$argc` seeds the by-value capture fixture so constant propagation cannot fold the captured +//! string into the literal and bypass the capture path entirely. + +use crate::support::*; + +/// Verifies the reported repro: a closure returning `[$a, $b]` from two `mixed` parameters +/// keeps the second argument's string type instead of casting it to `int(0)`. +#[test] +fn test_closure_returns_array_literal_of_mixed_params() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"z\"\n}\n" + ); +} + +/// Verifies the same closure shape with the argument order reversed, so the mis-stamped slot +/// is the first element rather than the second. +#[test] +fn test_closure_returns_array_literal_string_first_int_second() { + let out = compile_and_run( + r#"\n string(1) \"z\"\n [1]=>\n int(1)\n}\n" + ); +} + +/// Verifies a three-element literal of `mixed` parameters preserves int, string, and float +/// elements together. +#[test] +fn test_closure_returns_three_element_array_literal_of_mixed_params() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"z\"\n [2]=>\n float(2.5)\n}\n" + ); +} + +/// Verifies an arrow function body, which is the same single-`return` shape, gets the same +/// element typing as the closure form. +#[test] +fn test_arrow_function_returns_array_literal_of_mixed_params() { + let out = compile_and_run( + r#" [$a, $b]; +var_dump($f(1, "z")); +"#, + ); + assert_eq!( + out, + "array(2) {\n [0]=>\n int(1)\n [1]=>\n string(1) \"z\"\n}\n" + ); +} + +/// Pins the named-function form, which reads its return type from checker metadata and was +/// always correct, so the closure fix cannot regress it. +#[test] +fn test_named_function_returns_array_literal_of_mixed_params() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"z\"\n}\n" + ); +} + +/// Pins the instance-method and static-method forms of the same literal, which also read +/// checker metadata rather than the closure inference. +#[test] +fn test_methods_return_array_literal_of_mixed_params() { + let out = compile_and_run( + r#"pair(1, "z")); +var_dump(Box::spair("z", 1)); +"#, + ); + assert_eq!( + out, + "array(2) {\n [0]=>\n int(1)\n [1]=>\n string(1) \"z\"\n}\n\ + array(2) {\n [0]=>\n string(1) \"z\"\n [1]=>\n int(1)\n}\n" + ); +} + +/// Verifies a by-value captured string survives the returned literal. `$argc` keeps the +/// capture runtime-unknown so constant propagation cannot fold it into the literal. +#[test] +fn test_closure_returns_array_literal_with_by_value_capture() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"v\"\n}\n" + ); +} + +/// Verifies a by-reference captured string survives the returned literal and that a later +/// write through the reference is observed by a second call. +#[test] +fn test_closure_returns_array_literal_with_by_reference_capture() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(5) \"byref\"\n}\n\ + array(2) {\n [0]=>\n int(2)\n [1]=>\n string(7) \"changed\"\n}\n" + ); +} + +/// Pins the two shapes that already worked: the literal assigned to a local before it is +/// returned, and a closure with an explicit `: array` return type. +#[test] +fn test_closure_array_literal_via_local_and_declared_return_type() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"z\"\n}\n\ + array(2) {\n [0]=>\n string(1) \"z\"\n [1]=>\n int(1)\n}\n" + ); +} + +/// Verifies non-`mixed` declared parameter types are stamped from the signature too: a +/// `string`, `float`, or `bool` parameter used to be coerced to the syntactic `int` default. +#[test] +fn test_closure_returns_array_literal_of_typed_scalar_params() { + let out = compile_and_run( + r#"\n string(2) \"hi\"\n}\n\ + array(1) {\n [0]=>\n float(2.5)\n}\n\ + array(1) {\n [0]=>\n bool(true)\n}\n" + ); +} diff --git a/tests/codegen/callables/expr_calls.rs b/tests/codegen/callables/expr_calls.rs index c38f0f0470..05df5f3cda 100644 --- a/tests/codegen/callables/expr_calls.rs +++ b/tests/codegen/callables/expr_calls.rs @@ -6,12 +6,15 @@ //! //! Key details: //! - Inline PHP fixtures are compiled to native binaries and assertions compare stdout or expected failures. +//! - The trailing group pins the result storage of a builtin reached through a callable +//! binding: every dispatch form must agree with the direct call, because a mislabelled +//! result silently boxed a raw scalar or array return. use crate::support::*; /// Returns true when assembly contains a valid invokable object dispatch path. fn asm_has_invokable_object_call(user_asm: &str, class_name: &str) -> bool { - let eir_method = format!("_method_{}__u__u_invoke", class_name); + let eir_method = elephc::names::method_symbol(class_name, "__invoke"); (user_asm.contains("callable_instance_method") && user_asm.contains("callable_invoker")) || user_asm.contains(&eir_method) } @@ -1747,3 +1750,84 @@ echo $adapter->run(); out.stderr ); } + +/// Regression: a builtin reached through `call_user_func()` must be typed from its own +/// descriptor, not from the checker's result type for the `call_user_func()` call itself. +/// +/// The checker cannot resolve a callback held in a variable, so it types the call +/// runtime-opaque `mixed`; constant propagation then turns the variable into a literal and +/// lowering *does* resolve it. Reading the checker's per-span type back for the resolved +/// builtin labelled `array_reverse`'s raw array pointer as a boxed Mixed cell, and +/// `var_dump()` printed `bool(true)`. Expected output is verbatim `LC_ALL=C php` 8.4.20. +#[test] +fn test_call_user_func_variable_builtin_name_keeps_array_result_layout() { + let out = compile_and_run( + r#"\n int(3)\n [1]=>\n int(2)\n [2]=>\n int(1)\n}\n" + ); +} + +/// Regression: every `array_slice()` dispatch form agrees on the result layout. +/// +/// A variable-held callback previously reached the backend typed `mixed` and aborted with +/// `unsupported EIR backend feature: array_slice result PHP type Mixed`, while the direct, +/// value-call and literal-callback forms compiled. Expected output is verbatim +/// `LC_ALL=C php` 8.4.20. +#[test] +fn test_array_slice_dispatch_forms_agree_on_result_layout() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"a\"\n [2]=>\n float(1.5)\n [3]=>\n NULL\n [4]=>\n bool(true)\n}\n" + ); +} + +/// Verifies that declared parameters are included in the argument list and counted, and +/// that surplus positional arguments extend both. +#[test] +fn test_func_get_args_includes_declared_params() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"x\"\n}\n4|array(4) {\n [0]=>\n int(1)\n [1]=>\n string(1) \"x\"\n [2]=>\n float(3.5)\n [3]=>\n NULL\n}\n" + ); +} + +/// Verifies that `func_get_arg()` reads a surplus argument by zero-based position. +#[test] +fn test_func_get_arg_reads_surplus_argument() { + let out = compile_and_run( + r#"getMessage(); } +} +echo pick(1, 2); +"#, + ); + assert_eq!( + out, + "ValueError: func_get_arg(): Argument #1 ($position) must be less than the number of the arguments passed to the currently executed function" + ); +} + +/// Verifies php-src's separate `ValueError` message for a negative position. +#[test] +fn test_func_get_arg_negative_position_throws_value_error() { + let out = compile_and_run( + r#"getMessage(); } +} +echo pick(1, 2); +"#, + ); + assert_eq!( + out, + "ValueError: func_get_arg(): Argument #1 ($position) must be greater than or equal to 0" + ); +} + +/// Verifies PHP's "current values" rule: `func_get_args()` reflects a parameter that the +/// body reassigned, and a by-reference parameter it wrote through. +#[test] +fn test_func_get_args_reports_current_parameter_values() { + let out = compile_and_run( + r#"\n int(42)\n}\narray(1) {\n [0]=>\n int(99)\n}\nint(99)\n" + ); +} + +/// Verifies the constructs inside instance and static methods, which have their own +/// argument frame. +#[test] +fn test_func_args_in_methods() { + let out = compile_and_run( + r#"m(1, "b", 3.5), "|"; +var_dump(K::s(9, null)); +"#, + ); + assert_eq!( + out, + "3:3|array(2) {\n [0]=>\n int(9)\n [1]=>\n NULL\n}\n" + ); +} + +/// Verifies the constructs inside closures, whose argument frame is separate from the +/// enclosing scope's. +#[test] +fn test_func_args_in_closures() { + let out = compile_and_run( + r#"\n int(1)\n [1]=>\n string(1) \"z\"\n}\n" + ); +} + +/// Verifies that the surplus arguments are collected the same way whether they arrive +/// through an argument unpack or through `call_user_func`. +#[test] +fn test_func_num_args_counts_spread_and_call_user_func() { + let out = compile_and_run( + r#"\n string(1) \"a\"\n [1]=>\n string(1) \"b\"\n}\n" + ); +} + +/// Verifies iterating the argument list, the idiomatic "sum every argument" use. +#[test] +fn test_func_get_args_is_iterable() { + let out = compile_and_run( + r#"int cast edge cases --- + +/// Verifies `(int)` and `intval()` on NaN/±INF return `0` like PHP, on both supported targets. +/// +/// AArch64 `fcvtzs` saturates and x86_64 `cvttsd2si` returns `INT64_MIN`, so both targets used +/// to disagree with PHP and with each other. Values go through `$argc` so the folders cannot +/// evaluate the cast at compile time. +#[test] +fn test_cast_int_from_nan_and_infinity_is_zero() { + let out = compile_and_run( + r#" 100 ? "z" : ""); +var_dump(intval($text, 0)); +var_dump(intval($text, base: 16)); +"#, + ); + assert_eq!( + out, + "int(34)\nint(42)\nint(42)\nint(1)\nint(0)\nint(26)\nint(26)\n" + ); +} diff --git a/tests/codegen/casts_and_constants/math_builtins.rs b/tests/codegen/casts_and_constants/math_builtins.rs index 39cee1be9c..ff23769bf2 100644 --- a/tests/codegen/casts_and_constants/math_builtins.rs +++ b/tests/codegen/casts_and_constants/math_builtins.rs @@ -97,6 +97,58 @@ fn test_rand_no_args() { assert_eq!(out, "ok"); } +/// Verifies `random_int()` rejects an inverted range with PHP's catchable `ValueError`. +/// +/// The lowering computed the sample width as `max - min + 1`; an inverted range made that +/// non-positive and `__rt_random_uniform` handed back an unbounded garbage integer instead of a +/// value inside the requested range. +#[test] +fn test_random_int_inverted_range_is_a_catchable_value_error() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|", random_int(7, 7); +"#, + ); + assert_eq!( + out, + "ValueError|random_int(): Argument #1 ($min) must be less than or equal to argument #2 ($max)|7" + ); +} + +/// Verifies an uncaught inverted `random_int()` range reports PHP's uncaught-`ValueError` fatal. +#[test] +fn test_random_int_inverted_range_uncaught_reports_value_error_fatal() { + let err = compile_and_run_expect_failure("getMessage(); +} +$r = rand(10, 5); +echo "|", ($r >= 5 && $r <= 10) ? "in-range" : "out"; +"#, + ); + assert_eq!( + out, + "ValueError|mt_rand(): Argument #2 ($max) must be greater than or equal to argument #1 ($min)|in-range" + ); +} + // --- number_format --- /// Verifies `number_format(1234567)` formats with default 0 decimals, comma thousands separator: expects `1,234,567`. @@ -113,6 +165,70 @@ fn test_number_format_with_decimals() { assert_eq!(out, "1,234.57"); } +/// Verifies negative `$decimals` round to fewer significant digits instead of emitting garbage. +/// +/// PHP does not reject a negative precision: it pre-rounds the magnitude to that power of ten +/// (half away from zero) and then formats with no decimals, so `-4.9` with `-1` decimals is +/// `"0"` and never `"-0"`. elephc used to build the format string as `'0' + $decimals`, which +/// turned `-1` into the literal `"%./f"` and printed `"/f"`. +#[test] +fn test_number_format_negative_decimals_round_to_significant_digits() { + let out = compile_and_run( + r#" = out.lines().collect(); + assert_eq!(lines.len(), 16, "expected 8 folded and 8 runtime lines: {out:?}"); + assert_eq!(&lines[..8], &lines[8..], "folded and runtime `**` disagree"); + assert_eq!(lines[0], "int(8)"); + assert_eq!(lines[2], "float(9.223372036854776E+18)"); +} diff --git a/tests/codegen/cli.rs b/tests/codegen/cli.rs index 8d73b35e72..27a0771221 100644 --- a/tests/codegen/cli.rs +++ b/tests/codegen/cli.rs @@ -765,3 +765,61 @@ echo 1 + 2; let _ = fs::remove_dir_all(&dir); } + +/// Verifies `--debug-info` survives a source path that carries assembler string +/// metacharacters. A `\` used to be spliced into `.file`/`.asciz` unescaped, so +/// the assembler rejected the module outright; combined with `"` it terminated +/// the directive string early and let the rest of the path be assembled as +/// directives. The full compile must now succeed and the program must run. +#[test] +fn test_cli_debug_info_escapes_metacharacters_in_source_path() { + let dir = make_cli_test_dir("elephc_cli_debug_info_escapes"); + // A backslash alone broke the assembler; `\"` was the directive-injection + // vector. Both are legal filename bytes on every supported target. + let php_path = dir.join("bs\\la\"sh.php"); + fs::write( + &php_path, + r#" $value` binding. +#[test] +fn test_alternative_foreach_key_value() { + let out = compile_and_run("1,\"b\"=>2] as $k => $v): echo \"$k$v\"; endforeach;"); + assert_eq!(out, "a1b2"); +} + +/// Verifies the alternative `while` form loops until its condition is false. +#[test] +fn test_alternative_while() { + let out = compile_and_run(" 0): return \"pos\"; else: return \"nonpos\"; endif; } echo f(1), f(-1);", + ); + assert_eq!(out, "posnonpos"); +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 98bfae16b7..d56f72c115 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8432,6 +8432,25 @@ echo ":"; echo function_exists("strpos"); echo function_exists("strrpos");'); assert_eq!(out, "2:4:F:0:3:1:3:11"); } +/// Verifies eval honors PHP's third `$offset` argument on both position builtins, including +/// the named-argument spelling and `strrpos()`'s negative-offset rule (which bounds where a +/// match may end rather than where the scan starts). +/// Expected output is verbatim `LC_ALL=C php` 8.4 output for the same program. +#[test] +fn test_eval_string_position_builtins_honor_offset_argument() { + let out = compile_and_run( + r#" $word) { echo $offset, "=", $word, ";"; } +echo implode(",", str_word_count("fri3nd", 1, "3")), ":"; +echo implode(",", str_word_count("-abc-", 1));'); +"#, + ); + + assert_eq!(out, "4:Hello,friend:0=one;4=two;fri3nd:abc"); +} + +/// Verifies the eval interpreter reproduces every `count_chars()` mode and raises php-src's +/// catchable `ValueError` for an unknown one. +#[test] +fn test_eval_count_chars_parity() { + let out = compile_and_run( + r#" $count) { echo $byte, "=", $count, ";"; } +echo ":", count_chars("hello world", 3), ":", strlen(count_chars("hello world", 4)), ":", count(count_chars("aab", 0)); +try { count_chars("ab", 7); } catch (\ValueError $e) { echo ":", $e->getMessage(); }'); +"#, + ); + + assert_eq!( + out, + "101=1;104=1;108=2;111=1;: dehlorw:248:256:count_chars(): Argument #2 ($mode) must be between 0 and 4 (inclusive)" + ); +} + +/// Verifies the eval interpreter reproduces both `strtr()` shapes, including longest-match-first +/// selection, integer keys, and keys longer than the subject. +#[test] +fn test_eval_strtr_parity() { + let out = compile_and_run( + r#""bar","bar"=>"baz"]), ":"; +echo strtr("abc", ["a"=>"b","ab"=>"X"]), ":"; +echo strtr("12345", [1=>"one", 23=>"two-three"]), ":"; +echo strtr("abcd", "abc", "xy"), ":"; +echo strtr("abc", ["abcd"=>"X"]);'); +"#, + ); + + assert_eq!(out, "bar baz:Xc:onetwo-three45:xycd:abc"); +} + +/// Verifies the eval interpreter raises php-src's catchable `ValueError` for an unknown +/// `str_word_count()` format, matching the compiled backend's guard. +#[test] +fn test_eval_str_word_count_invalid_format_parity() { + let out = compile_and_run( + r#"getMessage(); }'); +"#, + ); + + assert_eq!( + out, + "str_word_count(): Argument #2 ($format) must be a valid format value" + ); +} + +/// Verifies the eval interpreter reproduces `file_get_contents()`'s `$offset`/`$length` window, +/// its unreachable-seek `false`, and its negative-`$length` `ValueError`, matching what the +/// compiled backend produces for the same reads. +/// +/// Both sides now declare the same five-parameter PHP 8.4 signature, so this fixture also pins +/// that the eval dispatcher accepts every argument position the static catalog advertises. +#[test] +fn test_eval_file_get_contents_offset_length_parity() { + let out = compile_and_run( + r#"getMessage(); }'); +unlink("eval_fgc.txt"); +"#, + ); + + assert_eq!( + out, + "DEFG:DEFG:HIJ:past-eof:EFG:file_get_contents(): Argument #5 ($length) must be greater than or equal to 0" + ); +} + +/// Verifies the eval interpreter and the compiled backend agree on `array_splice()`'s +/// `$replacement`: the same removed slice and the same mutated receiver on both sides. +#[test] +fn test_eval_array_splice_replacement_parity() { + let out = compile_and_run( + r#"getMessage(); } +echo '|'; +$a = 7; +try { $a %= $z; } catch (DivisionByZeroError $e) { echo 'compound:', $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "DivisionByZeroError:Modulo by zero|compound:Modulo by zero" + ); +} + +/// Verifies `/` by zero throws a catchable `DivisionByZeroError` for int and float operands. +/// +/// PHP throws for `1/0`, `1.0/0`, `1/0.0`, `0/0`, and `-1.0/0.0` alike — the IEEE `INF`/`NaN` +/// result is only reachable through `fdiv()`. elephc used to hand back `INF`. +#[test] +fn test_division_by_zero_throws_for_int_and_float_operands() { + let out = compile_and_run( + r#"getMessage(); } +echo '|'; +try { echo 1.0 / $zf; } catch (DivisionByZeroError $e) { echo 'f:', $e->getMessage(); } +echo '|'; +try { echo 1 / $zf; } catch (DivisionByZeroError $e) { echo 'if:', $e->getMessage(); } +echo '|'; +try { echo $zf / $zf; } catch (DivisionByZeroError $e) { echo 'ff:', $e->getMessage(); } +echo '|'; +try { echo -1.0 / $zf; } catch (DivisionByZeroError $e) { echo 'nf:', $e->getMessage(); } +echo '|'; +$b = 7; +try { $b /= $z; } catch (DivisionByZeroError $e) { echo 'compound:', $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "i:Division by zero|f:Division by zero|if:Division by zero|\ + ff:Division by zero|nf:Division by zero|compound:Division by zero" + ); +} + +/// Verifies the arithmetic `DivisionByZeroError` is a real `ArithmeticError`/`Throwable`. +#[test] +fn test_division_by_zero_error_matches_parent_handlers() { + let out = compile_and_run( + r#"getMessage(); } +"#, + ); + assert_eq!( + out, + "arithmetic|error|DivisionByZeroError|intdiv:Division by zero" + ); +} + +/// Verifies a negative shift count throws a catchable `ArithmeticError` for `<<` and `>>`. +/// +/// The hardware shift masks the count, so `1 << -1` used to evaluate to `PHP_INT_MIN`. +#[test] +fn test_negative_shift_count_throws_arithmetic_error() { + let out = compile_and_run( + r#"getMessage(); } +echo '|'; +try { echo (1 * $argc) >> $neg; } catch (ArithmeticError $e) { echo 'shr:', $e->getMessage(); } +echo '|'; +$c = 5; +try { $c <<= $neg; } catch (ArithmeticError $e) { echo 'compound:', $e->getMessage(); } +echo '|'; +try { echo (1 * $argc) << $neg; } catch (Throwable $e) { echo get_class($e); } +"#, + ); + assert_eq!( + out, + "ArithmeticError:Bit shift by negative number|\ + shr:Bit shift by negative number|\ + compound:Bit shift by negative number|ArithmeticError" + ); +} + +/// Verifies non-zero divisors and non-negative shift counts keep working after the guards. +#[test] +fn test_arithmetic_guards_do_not_disturb_normal_operands() { + let out = compile_and_run( + r#"> (1 * $n); +"#, + ); + assert_eq!(out, "1|1|4|3|3|INF|8|-4"); +} diff --git a/tests/codegen/generators/keys.rs b/tests/codegen/generators/keys.rs new file mode 100644 index 0000000000..f567bfc118 --- /dev/null +++ b/tests/codegen/generators/keys.rs @@ -0,0 +1,143 @@ +//! Purpose: +//! Regression tests for PHP's generator auto-key counter: how explicit `yield` +//! keys interact with the implicit numbering used by keyless yields, and why +//! `yield from` is exempt from that bookkeeping. +//! +//! Called from: +//! - `cargo test` via the integration test harness; aggregated under +//! `tests::codegen::generators` in `tests/codegen/generators/mod.rs`. +//! +//! Key details: +//! - PHP models the counter as `largest_used_integer_key` (initially -1): an +//! explicit *integer* key greater than the current largest becomes the new +//! largest, and every keyless `yield` emits `++largest`. Non-integer keys +//! (string, float, bool, null) and integer keys at or below the largest +//! leave it untouched, and the increment wraps at `PHP_INT_MAX`. +//! - `yield from` forwards the delegate's keys verbatim: it neither renumbers +//! them nor advances the outer generator's counter, so duplicate keys are +//! expected output rather than a bug. +//! - Every expected string in this module is real `LC_ALL=C php` 8.4 output. + +use crate::support::*; + +/// Verifies that an explicit integer key pushes the auto-key counter so the +/// following keyless yields continue the numbering instead of restarting at 0. +#[test] +fn test_generator_explicit_int_key_continues_auto_numbering() { + let out = compile_and_run( + r#" "five"; yield "six"; yield "seven"; } +foreach (keys() as $k => $v) { echo "[$k:$v]"; } +"#, + ); + assert_eq!(out, "[5:five][6:six][7:seven]"); +} + +/// Verifies that only keys greater than the largest integer key seen so far +/// move the counter: a lower explicit key is emitted as-is but never rewinds +/// the implicit numbering. +#[test] +fn test_generator_lower_explicit_key_does_not_rewind_counter() { + let out = compile_and_run( + r#" "a"; yield 2 => "b"; yield "c"; yield 40 => "d"; yield "e"; } +foreach (gen() as $k => $v) { echo "[$k:$v]"; } +"#, + ); + assert_eq!(out, "[10:a][2:b][11:c][40:d][41:e]"); +} + +/// Verifies that non-integer explicit keys are yielded unconverted (generators +/// do not apply array key coercion) and leave the auto-key counter alone. +#[test] +fn test_generator_non_integer_keys_leave_counter_untouched() { + let out = compile_and_run( + r#" 1; yield 2; yield 3.5 => 3; yield 4; yield true => 5; yield 6; yield null => 7; yield 8; } +foreach (gen() as $k => $v) { echo "["; var_export($k); echo ":$v]"; } +"#, + ); + assert_eq!(out, "['s':1][0:2][3.5:3][1:4][true:5][2:6][NULL:7][3:8]"); +} + +/// Verifies that negative explicit keys never move the counter, so the next +/// keyless yield still starts at 0 (PHP's largest-used key starts at -1). +#[test] +fn test_generator_negative_explicit_keys_keep_counter_at_zero() { + let out = compile_and_run( + r#" "a"; yield "b"; yield -1 => "c"; yield "d"; } +foreach (gen() as $k => $v) { echo "[$k:$v]"; } +"#, + ); + assert_eq!(out, "[-5:a][0:b][-1:c][1:d]"); +} + +/// Verifies that the counter wraps like PHP's signed 64-bit increment: a +/// `PHP_INT_MAX` key is followed by `PHP_INT_MIN`, not a float or an error. +#[test] +fn test_generator_auto_key_wraps_past_int_max() { + let out = compile_and_run( + r#" "a"; yield "b"; yield "c"; } +foreach (gen() as $k => $v) { echo "[$k:$v]"; } +"#, + ); + assert_eq!( + out, + "[9223372036854775807:a][-9223372036854775808:b][-9223372036854775807:c]" + ); +} + +/// Verifies that `yield from` forwards delegate keys verbatim — an inner +/// generator's own numbering and an inner array's indices both pass through +/// without renumbering and without advancing the outer counter, so the outer +/// generator's keys collide with the delegated ones exactly as in PHP. +#[test] +fn test_generator_yield_from_does_not_touch_outer_counter() { + let out = compile_and_run( + r#" "i1"; yield "i2"; } +function outer() { yield 3 => "o1"; yield from inner(); yield "o2"; yield from [7, 8]; yield "o3"; } +foreach (outer() as $k => $v) { echo "[$k:$v]"; } +"#, + ); + assert_eq!(out, "[3:o1][100:i1][101:i2][4:o2][0:7][1:8][5:o3]"); +} + +/// Verifies that the counter survives `send()`/`next()` resumptions and that a +/// generator with explicit keys still returns its `return` value, i.e. the +/// bookkeeping added to the suspend primitive does not disturb the resume path. +#[test] +fn test_generator_auto_key_survives_send_and_get_return() { + let out = compile_and_run( + r#" "p"; echo "<$x>"; yield "q"; yield 5 => "r"; yield "s"; return "done"; } +$g = gen(); +echo $g->key(), ";"; +$g->send("A"); +echo $g->key(), ";"; +$g->next(); +echo $g->key(), ";"; +$g->next(); +echo $g->key(), ";"; +$g->next(); +echo $g->getReturn(); +"#, + ); + assert_eq!(out, "20;21;5;22;done"); +} + +/// Verifies the runtime-typed key path: keys read out of an array (boxed Mixed +/// cells whose tag is only known at run time) update the counter when they hold +/// integers and are ignored when they hold strings. +#[test] +fn test_generator_runtime_typed_keys_update_counter() { + let out = compile_and_run( + r#" "v"; } yield "tail"; yield $n => "z"; yield "last"; } +foreach (gen(2) as $k => $v) { echo "["; var_export($k); echo ":$v]"; } +"#, + ); + assert_eq!(out, "[3:v]['s':v][1:v][8:v][9:tail][2:z][10:last]"); +} diff --git a/tests/codegen/generators/methods.rs b/tests/codegen/generators/methods.rs new file mode 100644 index 0000000000..07adcf8cba --- /dev/null +++ b/tests/codegen/generators/methods.rs @@ -0,0 +1,97 @@ +//! Purpose: +//! Regression tests for generator *methods*: a class method whose body contains +//! `yield` must be typed as returning a `Generator`, exactly like a generator +//! function, whether or not it carries a `: Generator` return hint. +//! +//! Called from: +//! - `cargo test` via the integration test harness; aggregated under +//! `tests::codegen::generators` in `tests/codegen/generators/mod.rs`. +//! +//! Key details: +//! - Before the fix the checker's method pass inferred `void` for an unhinted +//! generator method (its body has no value `return`), so `foreach` over the +//! call warned "null given" and never ran the loop; a `: Generator` hint hit +//! the "must return a value on every path" coverage check and failed to +//! compile at all. Free functions were unaffected, so every fixture here +//! iterates a *method* result. +//! - Expected values are real `LC_ALL=C php` 8.4 output. + +use crate::support::*; + +/// Verifies that a method whose body yields, declared with no return hint at +/// all, is still typed as a `Generator`: `foreach` over the call iterates the +/// yielded values with PHP's auto-incrementing keys instead of warning that the +/// method returned null. +#[test] +fn test_generator_method_without_return_hint_iterates() { + let out = compile_and_run( + r#"items as $i) { yield $i; } } +} +foreach ((new Box)->items() as $k => $v) { echo "$k:$v "; } +"#, + ); + assert_eq!(out, "0:1 1:2 2:3 "); +} + +/// Verifies the same shape with an explicit `: Generator` return hint, plus a +/// static generator method and a generator method with a `return` value read +/// back through `getReturn()`. The hint used to trip the declared-return +/// coverage check, which a generator body legitimately cannot satisfy. +#[test] +fn test_generator_method_with_generator_return_hint_iterates() { + let out = compile_and_run( + r#"items as $i) { yield $i; } } + public static function letters(): Generator { yield "a"; yield "b"; } + public function tally(): Generator { yield 1; return 7; } +} +$b = new Box(); +foreach ($b->items() as $k => $v) { echo "$k:$v "; } +foreach (Box::letters() as $k => $v) { echo "$k:$v "; } +$g = $b->tally(); +foreach ($g as $v) { echo $v; } +echo " ", $g->getReturn(); +"#, + ); + assert_eq!(out, "0:4 1:5 0:a 1:b 1 7"); +} + +/// Verifies generator methods reached through a trait and through an abstract +/// declaration: the trait method is flattened into the using class and the +/// override is checked against the abstract `: Generator` signature, so both +/// must survive the generator return-type override without a coverage error. +#[test] +fn test_generator_method_through_trait_and_abstract_override() { + let out = compile_and_run( + r#"two() as $k => $v) { echo "$k=$v "; } +foreach ((new Impl)->seq() as $k => $v) { echo "$k=$v "; } +"#, + ); + assert_eq!(out, "0=t0 1=t1 0=i0 "); +} + +/// Verifies a wider return hint that still accepts a `Generator` keeps working: +/// `iterable` is a supertype of `Generator`, so PHP accepts the declaration and +/// the method iterates normally. +#[test] +fn test_generator_method_with_iterable_return_hint() { + let out = compile_and_run( + r#"items() as $k => $v) { echo "$k:$v "; } +"#, + ); + assert_eq!(out, "0:1 1:2 "); +} diff --git a/tests/codegen/generators/mod.rs b/tests/codegen/generators/mod.rs index ae54fd0d6f..02d4fd1938 100644 --- a/tests/codegen/generators/mod.rs +++ b/tests/codegen/generators/mod.rs @@ -16,5 +16,7 @@ mod basic; mod control_flow; mod get_return; mod interop; +mod keys; +mod methods; mod send_throw; mod yield_from; diff --git a/tests/codegen/generators/yield_from.rs b/tests/codegen/generators/yield_from.rs index 477b5af8ba..2a06137696 100644 --- a/tests/codegen/generators/yield_from.rs +++ b/tests/codegen/generators/yield_from.rs @@ -323,3 +323,47 @@ foreach (combined() as $v) { echo $v; echo " "; } ); assert_eq!(out, "0 1 2 10 11 "); } + +/// Verifies `yield from` accepts an *associative* array literal, mixing an +/// explicit integer key with a string key. The desugared iterator loop already +/// handled hash storage; only the checker's `yield from` gate rejected the +/// literal, so this compiles and forwards both keys verbatim. +#[test] +fn test_generator_yield_from_assoc_array_literal() { + let out = compile_and_run( + r#" "x", "s" => "y"]; } +foreach (g() as $k => $v) { echo "$k=$v "; } +"#, + ); + assert_eq!(out, "5=x s=y "); +} + +/// Verifies a keyed literal whose key is a *computed* expression, delegated +/// between two ordinary yields. The forwarded keys must not advance the outer +/// generator's implicit-key counter: the bare `yield "b"` after the delegation +/// still gets key 1, not 31. +#[test] +fn test_generator_yield_from_computed_key_literal_keeps_outer_counter() { + let out = compile_and_run( + r#" "L", "k" => "M"]; yield "b"; } +foreach (g(3) as $k => $v) { echo "$k=$v "; } +"#, + ); + assert_eq!(out, "0=a 30=L k=M 1=b "); +} + +/// Verifies `yield from` over a local variable holding an associative array, +/// not just a literal. The delegated string keys pass through and the following +/// bare `yield` resumes the outer auto-key at 0. +#[test] +fn test_generator_yield_from_assoc_array_variable() { + let out = compile_and_run( + r#" 1, "q" => 2]; yield from $rows; yield 3; } +foreach (g() as $k => $v) { echo "$k=$v "; } +"#, + ); + assert_eq!(out, "p=1 q=2 0=3 "); +} diff --git a/tests/codegen/io/files.rs b/tests/codegen/io/files.rs index a7a5863cd6..650d25761d 100644 --- a/tests/codegen/io/files.rs +++ b/tests/codegen/io/files.rs @@ -191,3 +191,285 @@ unlink("ts.txt"); assert_eq!(out, "ok"); let _ = fs::remove_dir_all(&dir); } + +/// Verifies `file()`'s `$flags` bitmask over every combination PHP distinguishes. +/// +/// The fixture writes a file with two empty lines so `FILE_IGNORE_NEW_LINES` and +/// `FILE_SKIP_EMPTY_LINES` are separable: PHP applies the newline trimming FIRST, so +/// `FILE_SKIP_EMPTY_LINES` alone drops nothing (a bare `"\n"` line still has length 1). Each line +/// is reported as `index/strlen/trimmed-content` so the trailing-terminator handling is visible. +/// `FILE_USE_INCLUDE_PATH` is accepted and has no effect, matching PHP's default empty +/// `include_path`. The expected values are verbatim `LC_ALL=C php` output from PHP 8.4.20. +#[test] +fn test_file_flags_combinations() { + let (out, dir) = compile_and_run_in_dir( + r#" $l) { echo " ", $i, "/", strlen($l), "/", rtrim($l, "\r\n"); } echo "|"; } +dump("plain", file("f1.txt")); +dump("ignore", file("f1.txt", FILE_IGNORE_NEW_LINES)); +dump("skip", file("f1.txt", FILE_SKIP_EMPTY_LINES)); +dump("both", file("f1.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); +dump("incpath", file("f1.txt", FILE_USE_INCLUDE_PATH)); +unlink("f1.txt"); +"#, + ); + assert_eq!( + out, + "plain:5 0/6/alpha 1/1/ 2/5/beta 3/1/ 4/5/gamma|\ +ignore:5 0/5/alpha 1/0/ 2/4/beta 3/0/ 4/5/gamma|\ +skip:5 0/6/alpha 1/1/ 2/5/beta 3/1/ 4/5/gamma|\ +both:3 0/5/alpha 1/4/beta 2/5/gamma|\ +incpath:5 0/6/alpha 1/1/ 2/5/beta 3/1/ 4/5/gamma|" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `file()` accepts its `$flags` as a named argument and as a run-time value. +/// +/// The flag is a plain bitmask rather than a shape-changing literal, so a variable must work. +#[test] +fn test_file_flags_named_and_runtime() { + let (out, dir) = compile_and_run_in_dir( + r#" $l) { echo " ", $i, "/", strlen($l), "/", rtrim($l, "\r\n"); } echo "|"; } +dump("named", file(filename: "f1.txt", flags: FILE_IGNORE_NEW_LINES)); +$f = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; +dump("runtime", file("f1.txt", $f)); +unlink("f1.txt"); +"#, + ); + assert_eq!( + out, + "named:5 0/5/alpha 1/0/ 2/4/beta 3/0/ 4/5/gamma|runtime:3 0/5/alpha 1/4/beta 2/5/gamma|" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `FILE_IGNORE_NEW_LINES` removes a CRLF pair, not just the line feed. +#[test] +fn test_file_flags_strip_crlf() { + let (out, dir) = compile_and_run_in_dir( + r#" $l) { echo " ", $i, "/", strlen($l), "/", rtrim($l, "\r\n"); } echo "|"; } +dump("crlf", file("f2.txt", FILE_IGNORE_NEW_LINES)); +dump("crlfboth", file("f2.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)); +unlink("f2.txt"); +"#, + ); + assert_eq!(out, "crlf:4 0/1/a 1/1/b 2/0/ 3/1/c|crlfboth:3 0/1/a 1/1/b 2/1/c|"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies PHP's `$offset`/`$length` window on `file_get_contents()` for every shape reference +/// PHP accepts: a positive offset, an offset plus a length, a negative offset counted from the +/// end, a negative offset with a length, a length that runs past EOF, an offset past EOF, an +/// offset exactly at EOF, and a zero length. +/// +/// The expected block is verbatim `LC_ALL=C php` 8.4 output for the same fixture. +#[test] +fn test_file_get_contents_offset_and_length_match_php() { + let (out, dir) = compile_and_run_in_dir( + r#"getMessage(), "\n"; +} +try { + file_get_contents("does-not-exist.txt", false, null, 0, -5); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +unlink("neg.txt"); +"#, + ); + assert!(out.success, "program failed: {}", out.stderr); + assert_eq!( + out.stdout, + "ValueError: file_get_contents(): Argument #5 ($length) must be greater than or equal to 0\nValueError: file_get_contents(): Argument #5 ($length) must be greater than or equal to 0\n" + ); + assert!( + !out.stderr.contains("Failed to open stream"), + "the negative-length ValueError must precede the open, got stderr={}", + out.stderr + ); +} + +/// Verifies `$use_include_path = true` is accepted and reads the same file. +/// +/// elephc resolves paths against the current directory only — the same thing an include path of +/// `"."` does — so `true` and `false` cannot differ here. +#[test] +fn test_file_get_contents_use_include_path_reads_the_same_file() { + let (out, dir) = compile_and_run_in_dir( + r#"int conversion. #[path = "math/functions.rs"] mod functions; +#[path = "math/php_float_to_int.rs"] +mod php_float_to_int; +#[path = "math/rounding_modes.rs"] +mod rounding_modes; diff --git a/tests/codegen/math/functions.rs b/tests/codegen/math/functions.rs index 49cf8774b3..c376e9d477 100644 --- a/tests/codegen/math/functions.rs +++ b/tests/codegen/math/functions.rs @@ -370,3 +370,484 @@ fn test_checked_op_constant_folds_no_overflow_var_dump() { let out = compile_and_run(r#" 9.223372036854e18); +var_dump(abs(PHP_INT_MIN) > 9.223372036854e18); +var_dump(gettype(abs(PHP_INT_MIN))); +"#, + ); + assert_eq!( + out, + "string(6) \"double\"\nbool(true)\nbool(true)\nstring(6) \"double\"\n" + ); +} + +/// Verifies `abs()` on a boxed Mixed `PHP_INT_MIN` payload promotes to float as well. +/// +/// Runtime int arithmetic that can overflow is typed `Mixed`, so `abs()` on it goes through +/// `__rt_abs_mixed` rather than the inline integer lowering. +#[test] +fn test_abs_mixed_int_min_promotes_to_float() { + let out = compile_and_run( + r#" 9.223372036854e18); +"#, + ); + assert_eq!(out, "string(6) \"double\"\nbool(true)\n"); +} + +/// Verifies every non-overflowing `abs()` input keeps PHP's exact value and type. +#[test] +fn test_abs_keeps_int_and_float_results() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +$empty = []; +try { var_dump(max($empty)); } catch (ValueError $e) { echo get_class($e), ": ", $e->getMessage(), "\n"; } +try { min([]); } catch (ValueError $e) { echo "discarded: ", $e->getMessage(), "\n"; } +"#, + ); + assert_eq!( + out, + "ValueError: min(): Argument #1 ($value) must contain at least one element\n\ + ValueError: max(): Argument #1 ($value) must contain at least one element\n\ + discarded: min(): Argument #1 ($value) must contain at least one element\n" + ); +} + +/// Verifies the single-array `min()` / `max()` form over an indexed `array`, +/// including PHP's numeric-string promotion (`"10" < "9"` is false because both are +/// numeric, while `"10" < "9a"` falls back to a byte comparison). Expected output is +/// verbatim `LC_ALL=C php` 8.4 output for the same program. +#[test] +fn test_min_max_single_array_of_strings() { + let out = compile_and_run( + r#" 3, "b" => 1, "c" => 2]), max(["a" => 3, "b" => 1, "c" => 2])); +$h = ["x" => "pear", "y" => "apple"]; +var_dump(min($h), max($h)); +var_dump(min(["a" => 1.5, "b" => 2.5]), max(["a" => 1.5, "b" => 2.5])); +var_dump(min(["a" => null, "b" => 1, "c" => "z"]), max(["a" => null, "b" => 1, "c" => "z"])); +"#, + ); + assert_eq!( + out, + "int(1)\nint(3)\n\ + string(5) \"apple\"\nstring(4) \"pear\"\n\ + float(1.5)\nfloat(2.5)\n\ + NULL\nstring(1) \"z\"\n" + ); +} + +/// Verifies the container reductions on arrays built at run time from `$argc`, so the +/// elements survive constant folding and the loop really walks runtime storage. The +/// string result is copied out of the container, so it stays valid after the argument +/// temporary is released. Expected output is verbatim `LC_ALL=C php` 8.4 output. +#[test] +fn test_min_max_single_array_built_at_runtime() { + let out = compile_and_run( + r#"\n string(1) \"a\"\n [1]=>\n string(2) \"12\"\n [2]=>\n string(4) \"1010\"\n}\n\ +array(3) {\n [0]=>\n int(255)\n [1]=>\n int(15)\n [2]=>\n float(1.8446744073709552E+19)\n}\n\ +a/12\n" + ); +} + +/// Verifies `base_convert()` re-renders numerals across bases, ignoring characters that are +/// not digits of the source base and treating letter digits case-insensitively. +#[test] +fn test_base_convert() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { base_convert("f", 37, 10); } catch (\ValueError $e) { echo $e->getMessage(), "\n"; } +try { base_convert("f", 16, 1); } catch (\ValueError $e) { echo $e->getMessage(), "\n"; } +try { base_convert("f", 16, 37); } catch (\ValueError $e) { echo $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "base_convert(): Argument #2 ($from_base) must be between 2 and 36 (inclusive)\n\ + base_convert(): Argument #2 ($from_base) must be between 2 and 36 (inclusive)\n\ + base_convert(): Argument #3 ($to_base) must be between 2 and 36 (inclusive)\n\ + base_convert(): Argument #3 ($to_base) must be between 2 and 36 (inclusive)" + ); +} diff --git a/tests/codegen/math/php_float_to_int.rs b/tests/codegen/math/php_float_to_int.rs new file mode 100644 index 0000000000..904acf6277 --- /dev/null +++ b/tests/codegen/math/php_float_to_int.rs @@ -0,0 +1,178 @@ +//! Purpose: +//! Regression tests for PHP's `float`→`int` conversion (`zend_dval_to_lval`) and for the +//! shared `__rt_php_float_to_int` runtime helper that implements it on every supported target. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Fixtures multiply by `$argc` (1 when the compiled binary runs with no CLI arguments) so +//! the AST/EIR constant folders cannot evaluate the conversion at compile time; the point is +//! to exercise the runtime lowering. +//! - Reference PHP 8.4 maps NaN and ±INF to `0` and reduces every other out-of-range finite +//! double modulo 2^64. Raw hardware truncation does neither and, worse, disagrees between +//! AArch64 (`fcvtzs` saturates) and x86_64 (`cvttsd2si` yields `INT64_MIN`), so the helper's +//! assembly is asserted for *both* architectures from this host. + +use crate::support::*; +use elephc::codegen::platform::{Arch, Platform, Target}; + +/// Verifies `(int)` of NaN, ±INF, and huge finite doubles matches PHP 8.4 (all `0` here). +#[test] +fn test_float_to_int_out_of_range_is_zero() { + let out = compile_and_run( + r#"\n int(3)\n}\nbool(true)\n"); +} + +/// Returns the runtime assembly emitted for one supported target. +fn runtime_asm_for(arch: Arch, platform: Platform) -> String { + elephc::codegen::generate_runtime(8_388_608, Target { arch, platform }) +} + +/// Verifies the AArch64 runtime defines `__rt_php_float_to_int` with its integer decode. +/// +/// The helper must not fall back to a bare `fcvtzs`: that saturates instead of reducing modulo +/// 2^64 and returns `INT64_MAX`/`INT64_MIN` for out-of-range inputs. +#[test] +fn test_aarch64_runtime_defines_php_float_to_int() { + let asm = runtime_asm_for(Arch::AArch64, Platform::Linux); + assert!( + asm.contains("__rt_php_float_to_int:"), + "AArch64 runtime must define the shared PHP float->int helper" + ); + for expected in [ + "fmov x9, d0", + "ubfx x10, x9, #52, #11", + "and x11, x9, #0x000fffffffffffff", + "orr x11, x11, #0x0010000000000000", + "sub x10, x10, #1075", + "lsl x11, x11, x10", + "lsr x11, x11, x10", + "neg x9, x11", + ] { + assert!( + asm.contains(expected), + "AArch64 PHP float->int helper is missing `{expected}`" + ); + } +} + +/// Verifies the x86_64 runtime defines the same helper with the same IEEE-754 decode. +/// +/// This target cannot be executed from the macOS/AArch64 development host, so the emitted +/// instruction sequence is asserted directly to keep both lowerings in lockstep. +#[test] +fn test_x86_64_runtime_defines_php_float_to_int() { + let asm = runtime_asm_for(Arch::X86_64, Platform::Linux); + assert!( + asm.contains("__rt_php_float_to_int:"), + "x86_64 runtime must define the shared PHP float->int helper" + ); + for expected in [ + "movq r10, xmm0", + "and ecx, 0x7ff", + "bts r11, 52", + "sub rcx, 1075", + "shl r11, cl", + "shr r11, cl", + "neg r11", + ] { + assert!( + asm.contains(expected), + "x86_64 PHP float->int helper is missing `{expected}`" + ); + } +} + +/// Verifies the array/cast runtime helpers call the shared conversion instead of truncating. +/// +/// `__rt_mixed_cast_int` (PHP `(int)` on a boxed Mixed) and `__rt_array_set_mixed_key` (float +/// array keys) each used a bare `fcvtzs` / `cvttsd2si`, which is where the per-target `(int)NAN` +/// and `$a[INF]` divergence came from. +#[test] +fn test_runtime_float_consumers_call_the_shared_helper() { + for (arch, expected_calls) in [(Arch::AArch64, "bl __rt_php_float_to_int"), ( + Arch::X86_64, + "call __rt_php_float_to_int", + )] { + let asm = runtime_asm_for(arch, Platform::Linux); + assert!( + asm.matches(expected_calls).count() >= 4, + "{arch:?} runtime should route every float->int consumer through the shared helper" + ); + } +} diff --git a/tests/codegen/math/rounding_modes.rs b/tests/codegen/math/rounding_modes.rs new file mode 100644 index 0000000000..01abf4dbab --- /dev/null +++ b/tests/codegen/math/rounding_modes.rs @@ -0,0 +1,132 @@ +//! Purpose: +//! Integration tests for PHP 8.4's `round($num, $precision, $mode)` third argument and the +//! `PHP_ROUND_HALF_*` predefined constants. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected string is real `LC_ALL=C php` 8.4.20 output for the same fixture. +//! - `$argc`-derived values keep the call runtime-unknown so the AST/EIR folders cannot +//! replace the call with a literal and hide the `__rt_round_mode` lowering. + +use crate::support::*; + +/// Verifies the four `PHP_ROUND_HALF_*` constants resolve to php-src's integer values. +#[test] +fn test_round_half_constants_values() { + let out = compile_and_run( + r#"getMessage(); } +"#, + ); + assert_eq!( + out, + "ValueError|round(): Argument #3 ($mode) must be a valid rounding mode (RoundingMode::*)" + ); +} + +/// Verifies `count()`'s `$mode` argument, including `COUNT_RECURSIVE` over a flat array. +/// +/// A flat array's recursive count equals its normal count, so all four spellings agree. +#[test] +fn test_count_mode_flat_array() { + let out = compile_and_run( + r#"getMessage(); } +"#, + ); + assert_eq!( + out, + "ValueError|count(): Argument #2 ($mode) must be either COUNT_NORMAL or COUNT_RECURSIVE" + ); +} + +/// Verifies `COUNT_NORMAL` and `COUNT_RECURSIVE` carry php-src's integer values. +#[test] +fn test_count_mode_constants_values() { + let out = compile_and_run(r#"i(); +echo $t instanceof M\Thing ? "yes" : "no"; +"#, + ); + assert_eq!(out, "Thing::m7iyes"); +} + +/// Verifies that a namespace alias expands the leading segment of a qualified *constant* +/// reference (`M\FOO`). +#[test] +fn test_namespace_alias_expands_qualified_constant() { + let out = compile_and_run( + r#"i(); +"#, + ); + assert_eq!(out, "i"); +} diff --git a/tests/codegen/numeric_scalars.rs b/tests/codegen/numeric_scalars.rs index f74e0be09c..388b567c14 100644 --- a/tests/codegen/numeric_scalars.rs +++ b/tests/codegen/numeric_scalars.rs @@ -440,3 +440,94 @@ fn test_float_separator_exponent_echo() { let out = compile_and_run("\n float(1000000000000000)\n [1]=>\n float(-0)\n}\n" + ); +} + +/// Verifies `var_dump()` and `var_export()` agree on the same float rendering: both +/// implement `serialize_precision = -1`, one in the runtime (`__rt_ftoa_repr`) and one in +/// the injected elephc-PHP prelude, so a divergence between them is a real bug. The only +/// intended difference is `var_export`'s `.0` suffix on integer-valued floats. +#[test] +fn test_var_dump_and_var_export_float_rendering_agree() { + let out = compile_and_run( + r#"big = $h->big * $argc; +$h->flat = $h->flat * $argc; +print_r($h); +foreach ([$h->big, $h->eps, $h->flat, $h->neg] as $v) { var_dump($v); } +"#, + ); + assert_eq!( + out, + "Holder Object\n(\n [big] => 1.0E+17\n [eps] => 0.3\n [flat] => 1.0E+15\n [neg] => -0\n)\nfloat(1.0E+17)\nfloat(0.30000000000000004)\nfloat(1000000000000000)\nfloat(-0)\n" + ); +} diff --git a/tests/codegen/objects/classes.rs b/tests/codegen/objects/classes.rs index b24a76983d..461b78b16a 100644 --- a/tests/codegen/objects/classes.rs +++ b/tests/codegen/objects/classes.rs @@ -608,3 +608,90 @@ echo $u->id(); ); assert_eq!(out, "7"); } + +/// Verifies PHP's `==` between two objects: same class plus loosely equal properties. +/// +/// `===` (identity) must keep answering instance identity, a property-less class +/// compares equal for two distinct instances, differing property values and +/// differing classes compare unequal, and property comparison is LOOSE +/// (`Box(1) == Box("1")` and `Box(0) == Box(null)` are true). +#[test] +fn test_object_loose_equality_compares_class_then_properties() { + let out = compile_and_run( + r#"v = $v; } } +$e1 = new Empty1(); $e2 = new Empty1(); +var_dump($e1 == $e2, $e1 === $e2, $e1 === $e1); +$p1 = new Pt(); $p2 = new Pt(); +var_dump($p1 == $p2, $p1 === $p2); +$p2->x = 2; +var_dump($p1 == $p2, $p1 != $p2); +var_dump($p1 == new Pt2()); +var_dump(new Box(1) == new Box("1"), new Box(0) == new Box(null), new Box(1) == new Box(2)); +"#, + ); + assert_eq!( + out, + "bool(true)\nbool(false)\nbool(true)\n\ + bool(true)\nbool(false)\n\ + bool(false)\nbool(true)\n\ + bool(false)\n\ + bool(true)\nbool(true)\nbool(false)\n" + ); +} + +/// Verifies object `==` recurses through array-valued and object-valued properties, +/// and that enum cases keep PHP's compare-by-identity behavior. +#[test] +fn test_object_loose_equality_recurses_and_handles_enums() { + let out = compile_and_run( + r#"items = [1, 2, 4]; +var_dump($b1 == $b2); +$w1 = new Wrap(); $w2 = new Wrap(); +var_dump($w1 == $w2); +$w1->inner = new Pt(); +var_dump($w1 == $w2); +$w2->inner = new Pt(); +var_dump($w1 == $w2); +var_dump(Suit::Hearts == Suit::Hearts, Suit::Hearts == Suit::Spades, Suit::Hearts === Suit::Hearts); +var_dump(Grade::A == Grade::A, Grade::A == Grade::B); +"#, + ); + assert_eq!( + out, + "bool(true)\nbool(false)\n\ + bool(true)\nbool(false)\nbool(true)\n\ + bool(true)\nbool(false)\nbool(true)\n\ + bool(true)\nbool(false)\n" + ); +} + +/// Verifies a cyclic object graph does not make `==` recurse until the stack dies. +/// +/// PHP raises `Nesting level too deep - recursive dependency?`; elephc's walker caps +/// its depth and reports "not equal" instead (documented in `docs/php/classes.md`). +/// The regression this pins is that the program terminates normally. +#[test] +fn test_object_loose_equality_survives_recursive_dependency() { + let out = compile_and_run( + r#"self = $a; +$b = new Node(); $b->self = $b; +var_dump($a == $b); +echo "survived"; +"#, + ); + assert_eq!(out, "bool(false)\nsurvived"); +} diff --git a/tests/codegen/objects/property_access/by_ref_builtin_args.rs b/tests/codegen/objects/property_access/by_ref_builtin_args.rs new file mode 100644 index 0000000000..db8f90d980 --- /dev/null +++ b/tests/codegen/objects/property_access/by_ref_builtin_args.rs @@ -0,0 +1,96 @@ +//! Purpose: +//! Regression tests for passing an object property (or a container element reached through +//! one) as the by-reference argument of a mutating array builtin. These calls used to be +//! silent no-ops: the property's array was loaded, separated by copy-on-write inside the +//! runtime helper, mutated, and then discarded because nothing stored it back. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Expected values are real `LC_ALL=C php` 8.4 output for the same fixtures. +//! - The receiver shapes here cover the property-resolution paths the lowering has to walk: +//! a declared property, an inherited one, a constructor-promoted one, `self::$prop` from a +//! static method, and a property reached through a typed method parameter. + +use super::*; + +/// A constructor-promoted, declared-type property is still a writable place for a +/// by-reference builtin argument. +#[test] +fn test_usort_on_promoted_constructor_property() { + let out = compile_and_run( + r#"items, fn($x, $y) => $x <=> $y); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "1,2,3"); +} + +/// A property inherited from a parent class resolves through the same visible-property +/// lookup, so `sort()` mutates the subclass instance's storage. +#[test] +fn test_sort_on_inherited_property() { + let out = compile_and_run( + r#"items); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "1,2,3"); +} + +/// A `self::$prop` receiver inside a static method: the static receiver resolves to the +/// enclosing class before the write-back targets the same static slot. +#[test] +fn test_sort_on_self_static_property_inside_static_method() { + let out = compile_and_run( + r#"items); } } +$b = new B(); +(new S())->run($b); +echo implode(",", $b->items); +"#, + ); + assert_eq!(out, "3,2,1"); +} + +/// Two different array properties of the same object are mutated independently, so the +/// synthetic temporaries do not alias each other. +#[test] +fn test_sorting_two_properties_of_one_object() { + let out = compile_and_run( + r#"a); +rsort($o->b); +echo implode(",", $o->a), "|", implode(",", $o->b); +"#, + ); + assert_eq!(out, "1,2,3|6,5,4"); +} diff --git a/tests/codegen/objects/property_access/mod.rs b/tests/codegen/objects/property_access/mod.rs index 74e8258de9..4d5b50bae1 100644 --- a/tests/codegen/objects/property_access/mod.rs +++ b/tests/codegen/objects/property_access/mod.rs @@ -5,11 +5,13 @@ //! - `cargo test` through Rust's test harness. //! //! Key details: -//! - Submodules group focused fixtures for nullsafe property and method access, mutations, deep chains. +//! - Submodules group focused fixtures for nullsafe property and method access, mutations, deep chains, null-capable int property storage, and properties passed as by-reference arguments to mutating array builtins. use super::*; +mod by_ref_builtin_args; mod nullsafe; mod nullsafe_side_effects; mod mutations; mod deep_chains; +mod nullable_int_defaults; diff --git a/tests/codegen/objects/property_access/mutations.rs b/tests/codegen/objects/property_access/mutations.rs index cf4c624f07..8958b95cd1 100644 --- a/tests/codegen/objects/property_access/mutations.rs +++ b/tests/codegen/objects/property_access/mutations.rs @@ -474,3 +474,250 @@ echo $box->value; ); assert_eq!(out, "7"); } + +/// Verifies `unset($obj->prop)` on a declared (typed) property. +/// +/// PHP leaves the property UNINITIALIZED rather than nulled: `isset()` answers false, +/// `print_r` omits it, reading it raises `Error: Typed property … must not be accessed +/// before initialization`, and assigning again brings it back. `unset($a, $b)` clears +/// both targets. +#[test] +fn test_unset_declared_typed_property_leaves_it_uninitialized() { + let out = compile_and_run( + r#"n, $t->s); +var_dump(isset($t->n), isset($t->s), isset($t->a)); +print_r($t); +try { echo $t->n; } catch (\Error $e) { echo "ERR:", $e->getMessage(), "\n"; } +$t->n = 9; +var_dump(isset($t->n), $t->n); +"#, + ); + assert_eq!( + out, + "bool(false)\nbool(false)\nbool(true)\n\ + T Object\n(\n [a] => Array\n (\n [0] => 1\n [1] => 2\n )\n\n)\n\ + ERR:Typed property T::$n must not be accessed before initialization\n\ + bool(true)\nint(9)\n" + ); +} + +/// Verifies `unset()` on a property the caller cannot see still routes to `__unset`. +/// +/// PHP calls `__unset` only for an INACCESSIBLE (or absent) property; a property the +/// caller can see is removed directly and `__unset` is never consulted. +#[test] +fn test_unset_inaccessible_property_calls_magic_unset() { + let out = compile_and_run( + r#"secret); +unset($p->open); +var_dump(isset($p->open)); +"#, + ); + assert_eq!(out, "magic:secret\nbool(false)\n"); +} + +/// Verifies `unset($std->prop)` on a `stdClass` really REMOVES the dynamic property. +/// +/// Every `stdClass` property is a hash entry, so PHP's removal semantics are exact here: +/// `isset()` answers false, `json_encode()` stops listing the key, unsetting the same key +/// again and unsetting a key that was never set are both no-ops, a later write re-appends +/// the key at the END of the property order, `unset($o->b, $o->c)` removes both, and a read +/// of the removed name answers null (observed through `??`, so the fixture does not depend +/// on the undefined-property warning elephc does not yet emit for `stdClass`). +/// +/// Expected output is `LC_ALL=C php 8.4.20` verbatim. The fixture deliberately avoids +/// `var_dump($o)`/`print_r($o)`: elephc renders a `stdClass` body as empty regardless of +/// `unset()`, a separate pre-existing gap. +#[test] +fn test_unset_stdclass_dynamic_property_removes_it() { + let out = compile_and_run( + r#"a = 1; +$o->b = "two"; +$o->c = 3; +unset($o->a); +var_dump(isset($o->a), isset($o->b)); +echo json_encode($o), "\n"; +unset($o->a); +unset($o->never); +echo json_encode($o), "\n"; +$o->a = 9; +echo json_encode($o), "\n"; +echo $o->a, "|", $o->b, "\n"; +unset($o->b, $o->c); +echo json_encode($o), "\n"; +var_dump($o->b ?? "gone"); +"#, + ); + assert_eq!( + out, + "bool(false)\nbool(true)\n\ + {\"b\":\"two\",\"c\":3}\n\ + {\"b\":\"two\",\"c\":3}\n\ + {\"b\":\"two\",\"c\":3,\"a\":9}\n\ + 9|two\n\ + {\"a\":9}\n\ + string(4) \"gone\"\n" + ); +} + +/// Verifies `unset()` of an UNDECLARED name on an `#[AllowDynamicProperties]` class removes +/// the hash entry while leaving the class's fixed slots untouched. +/// +/// The receiver mixes both storage shapes: `$fixed` is a declared typed slot and `$x`/`$y` +/// are dynamic hash entries. Unsetting the dynamic names must not disturb `$fixed`, and +/// repeat/absent unsets stay no-ops. Expected output is `LC_ALL=C php 8.4.20` verbatim. +#[test] +fn test_unset_dynamic_property_on_allow_dynamic_class() { + let out = compile_and_run( + r#"x = 1; +$b->y = "two"; +unset($b->x); +var_dump(isset($b->x), isset($b->y), isset($b->fixed)); +unset($b->x); +unset($b->missing); +$b->x = 5; +var_dump(isset($b->x)); +echo $b->x, "|", $b->y, "|", $b->fixed, "\n"; +unset($b->x, $b->y); +var_dump(isset($b->x), isset($b->y), isset($b->fixed)); +echo $b->fixed, "\n"; +"#, + ); + assert_eq!( + out, + "bool(false)\nbool(true)\nbool(true)\n\ + bool(true)\n\ + 5|two|7\n\ + bool(false)\nbool(false)\nbool(true)\n\ + 7\n" + ); +} + +/// Regression: repeatedly reading the SAME dynamic property must keep answering its value. +/// +/// `__rt_hash_get` only borrows the stored `Mixed` cell, but the dynamic-property read +/// hands its result to a caller that releases it, so a missing retain made every read drop +/// a reference the program never took. After enough reads the live hash entry was freed and +/// further reads answered `NULL` — a use-after-free of the property's storage. +/// Expected output is `LC_ALL=C php 8.4.20` verbatim. +#[test] +fn test_repeated_dynamic_property_reads_keep_the_value_alive() { + let out = compile_and_run( + r#"v = "kept"; +echo $s->v, $s->v, $s->v, "\n"; +var_dump($s->v, $s->v); +var_dump($s->v); +"#, + ); + assert_eq!( + out, + "keptkeptkept\nstring(4) \"kept\"\nstring(4) \"kept\"\nstring(4) \"kept\"\n" + ); +} + +/// Verifies `unset()` of an UNTYPED declared property is refused with a diagnostic that +/// names that shape, instead of silently leaving a stale value behind. +/// +/// PHP genuinely removes such a property: a later read warns `Undefined property` and +/// answers `null`. elephc gives each declared property a fixed, monomorphically typed slot +/// (here `Int`), which has no encoding for "removed, and reading as null" — see +/// `docs/php/classes.md`. A loud compile error beats a wrong value. +#[test] +fn test_unset_untyped_declared_property_is_rejected() { + let error = compile_source_expect_backend_error( + r#"foo); +echo "ok"; +"#, + ); + assert!( + error.contains("An UNTYPED declared property"), + "the diagnostic must name the untyped-property shape, got: {}", + error + ); +} + +/// Verifies `unset()` of a BY-REFERENCE property is refused rather than silently skipped. +/// +/// The slot holds an object-owned ref-cell pointer that the destructor still frees and that +/// a later write would write THROUGH, reviving the alias PHP's `unset()` just broke. The +/// backend used to skip the shape quietly, which left `isset()` answering `true` after an +/// `unset()` where PHP answers `false`. +#[test] +fn test_unset_by_reference_property_is_rejected() { + let error = compile_source_expect_backend_error( + r#"p); +"#, + ); + assert!( + error.contains("unset() of by-reference property R::$p"), + "the diagnostic must name the by-reference property, got: {}", + error + ); +} + +/// Verifies `unset()` of a dynamic name on a class that declares `__unset()` is refused. +/// +/// PHP consults `__unset()` only when the dynamic property is ABSENT at the unset site and +/// removes the entry silently when it is present — a choice that depends on runtime state. +/// elephc picks the lowering statically, so it declines rather than guessing one of the two +/// behaviors. +#[test] +fn test_unset_dynamic_property_with_magic_unset_is_rejected() { + let error = compile_source_expect_backend_error( + r#"a = 1; +unset($h->a); +"#, + ); + assert!( + error.contains("unset target shape"), + "the runtime-dependent __unset shape must be refused, got: {}", + error + ); +} + +/// Verifies `isset()` on a never-initialized typed property answers false instead of +/// raising the uninitialized-read error, matching PHP. +#[test] +fn test_isset_on_uninitialized_typed_property_is_false() { + let out = compile_and_run( + r#"v), isset($u->w)); +$u->v = 5; +var_dump(isset($u->v), $u->v); +"#, + ); + assert_eq!(out, "bool(false)\nbool(true)\nbool(true)\nint(5)\n"); +} diff --git a/tests/codegen/objects/property_access/nullable_int_defaults.rs b/tests/codegen/objects/property_access/nullable_int_defaults.rs new file mode 100644 index 0000000000..1c3d4d1265 --- /dev/null +++ b/tests/codegen/objects/property_access/nullable_int_defaults.rs @@ -0,0 +1,263 @@ +//! Purpose: +//! Integration tests for null-capable int properties (`?int` / `int|null`), whose slots use the +//! inline two-word `{payload, tag}` TaggedScalar storage under `NullRepr::Tagged`. Pins the +//! literal-default initializer, the read paths, and the sibling nullable scalar types that must +//! stay on their existing representations. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Regression for the silent miscompile where a non-null literal default (`public ?int $p = 1;`) +//! was boxed into a Mixed cell and written into the payload word of a TaggedScalar slot, so the +//! reader handed the cell pointer back as an integer. +//! - Also pins the mixed-receiver property read (an object reached through a heterogeneous array), +//! which loaded the payload into the register still holding the object pointer and then +//! dereferenced it as the tag address. +//! - Every fixture is compiled with `compile_and_run_tagged` so the tagged representation is +//! exercised regardless of `ELEPHC_NULL_REPR`; expected outputs are `LC_ALL=C php` 8.4 output. + +use super::*; + +/// Verifies the reported repro: a `?int` property with a non-null literal default reads back as +/// that integer instead of the address of a boxed Mixed cell. +#[test] +fn test_nullable_int_property_non_null_default_reads_back() { + let out = compile_and_run_tagged( + r#"foo); +"#, + ); + assert_eq!(out, "int(1)\n"); +} + +/// Verifies negative and zero literal defaults on `?int` slots, since a negated literal takes a +/// different arm of the literal-default classifier than a plain integer literal. +#[test] +fn test_nullable_int_property_negative_and_zero_defaults() { + let out = compile_and_run_tagged( + r#"a); +var_dump($m->b); +"#, + ); + assert_eq!(out, "int(-7)\nint(0)\n"); +} + +/// Verifies the explicit `int|null` spelling takes the same inline tagged-scalar storage as `?int`, +/// on both an instance and a static property. +#[test] +fn test_explicit_int_null_union_property_defaults() { + let out = compile_and_run_tagged( + r#"a); +var_dump(M::$b); +$m->a = null; +var_dump($m->a); +"#, + ); + assert_eq!(out, "int(1)\nint(-2)\nNULL\n"); +} + +/// Verifies an explicit `= null` default on a `?int` slot still reads as null and is not `isset`. +#[test] +fn test_nullable_int_property_null_default() { + let out = compile_and_run_tagged( + r#"foo); +var_dump(isset($m->foo)); +"#, + ); + assert_eq!(out, "NULL\nbool(false)\n"); +} + +/// Verifies a `?int` slot round-trips through runtime assignments in both directions: int, back to +/// null, and back to another int. +#[test] +fn test_nullable_int_property_runtime_assignment_round_trip() { + let out = compile_and_run_tagged( + r#"foo = 99; +var_dump($m->foo); +$m->foo = null; +var_dump($m->foo); +$m->foo = 7; +var_dump($m->foo); +"#, + ); + assert_eq!(out, "int(99)\nNULL\nint(7)\n"); +} + +/// Verifies every ordinary reader over a non-null `?int` property: echo, print_r, interpolation, +/// arithmetic, strict comparison, is_null, `??`, string cast, and isset. +#[test] +fn test_nullable_int_property_readers_when_set() { + let out = compile_and_run_tagged( + r#"foo, "\n"; +print_r($m->foo); +echo "\n"; +echo "v={$m->foo}\n"; +var_dump($m->foo + 1); +var_dump($m->foo === 1); +var_dump(is_null($m->foo)); +var_dump($m->foo ?? 99); +var_dump((string) $m->foo); +var_dump(isset($m->foo)); +"#, + ); + assert_eq!( + out, + "1\n1\nv=1\nint(2)\nbool(true)\nbool(false)\nint(1)\nstring(1) \"1\"\nbool(true)\n" + ); +} + +/// Verifies the same readers over a `?int` property holding null: PHP renders null as the empty +/// string in echo/print_r/interpolation and reports it through the null-aware predicates. +#[test] +fn test_nullable_int_property_readers_when_null() { + let out = compile_and_run_tagged( + r#"foo = null; +echo "[", $m->foo, "]\n"; +print_r($m->foo); +echo "\n"; +echo "v={$m->foo}\n"; +var_dump($m->foo === null); +var_dump(is_null($m->foo)); +var_dump($m->foo ?? 99); +var_dump(isset($m->foo)); +"#, + ); + assert_eq!( + out, + "[]\n\nv=\nbool(true)\nbool(true)\nint(99)\nbool(false)\n" + ); +} + +/// Verifies a promoted constructor property typed `?int`: the promoted default, an explicit int +/// argument, and an explicit null argument. +#[test] +fn test_nullable_int_promoted_constructor_property() { + let out = compile_and_run_tagged( + r#"v); +var_dump((new P(9))->v); +var_dump((new P(null))->v); +"#, + ); + assert_eq!(out, "int(5)\nint(9)\nNULL\n"); +} + +/// Verifies `?int` static properties: both literal default forms and assignments in both +/// directions. Static slots take the same literal-default classifier as instance slots. +#[test] +fn test_nullable_int_static_property_defaults_and_assignment() { + let out = compile_and_run_tagged( + r#"foo); +$a->foo = 123; +$c = clone $a; +var_dump($c->foo); +$a->foo = null; +$d = clone $a; +var_dump($d->foo); +"#, + ); + assert_eq!(out, "int(1)\nint(123)\nNULL\n"); +} + +/// Verifies the sibling nullable scalar property types and `mixed` keep their existing +/// representations: only `?int` moves to the inline tagged-scalar storage. +#[test] +fn test_sibling_nullable_scalar_property_defaults_unaffected() { + let out = compile_and_run_tagged( + r#"v); +var_dump((new B())->v); +var_dump((new T())->v); +var_dump((new X())->v); +"#, + ); + assert_eq!(out, "float(1.5)\nbool(true)\nstring(2) \"hi\"\nint(1)\n"); +} + +/// Verifies a `?int` property read through an object stored in an array, both when the array is +/// homogeneous (a direct object slot) and when it is heterogeneous (a Mixed element whose read +/// goes through the runtime class dispatch). +#[test] +fn test_nullable_int_property_read_through_object_in_array() { + let out = compile_and_run_tagged( + r#"a); +$mixed = [new A(), new B()]; +var_dump($mixed[0]->a); +var_dump($mixed[1]->b); +"#, + ); + assert_eq!(out, "int(1)\nint(1)\nNULL\n"); +} + +/// Verifies the whole point of the tagged representation on property slots: the integer that +/// collides with the legacy in-band null sentinel (`PHP_INT_MAX - 1`) is a real value in an +/// instance and a static `?int` property, not null. +#[test] +fn test_nullable_int_property_default_at_sentinel_bit_pattern() { + let out = compile_and_run_tagged( + r#"foo); +var_dump(M::$bar); +var_dump($m->foo === 9223372036854775806); +var_dump(is_null($m->foo)); +"#, + ); + assert_eq!( + out, + "int(9223372036854775806)\nint(9223372036854775806)\nbool(true)\nbool(false)\n" + ); +} diff --git a/tests/codegen/operators.rs b/tests/codegen/operators.rs index f4ca3d5b26..377d95719e 100644 --- a/tests/codegen/operators.rs +++ b/tests/codegen/operators.rs @@ -10,7 +10,6 @@ use crate::support::*; // --- Phase 3: Arithmetic --- - /// Verifies integer addition with literal operands: 10 + 32 = 42. #[test] fn test_addition() { @@ -18,6 +17,7 @@ fn test_addition() { assert_eq!(out, "42"); } + /// Verifies integer subtraction with literal operands: 100 - 58 = 42. #[test] fn test_subtraction() { @@ -25,6 +25,7 @@ fn test_subtraction() { assert_eq!(out, "42"); } + /// Verifies integer multiplication with literal operands: 6 * 7 = 42. #[test] fn test_multiplication() { @@ -32,6 +33,7 @@ fn test_multiplication() { assert_eq!(out, "42"); } + /// Verifies integer division with literal operands: 84 / 2 = 42. #[test] fn test_division() { @@ -39,6 +41,7 @@ fn test_division() { assert_eq!(out, "42"); } + /// Verifies arithmetic with variables: loads two integers from memory and adds them. #[test] fn test_arithmetic_with_variables() { @@ -46,6 +49,7 @@ fn test_arithmetic_with_variables() { assert_eq!(out, "42"); } + /// Verifies operator precedence: multiplication binds tighter than addition, so 2 + 3 * 4 = 14. #[test] fn test_operator_precedence() { @@ -53,6 +57,7 @@ fn test_operator_precedence() { assert_eq!(out, "14"); } + /// Verifies parenthesized precedence: (2 + 3) * 4 = 20, confirming parentheses override default precedence. #[test] fn test_parenthesized_arithmetic() { @@ -60,6 +65,7 @@ fn test_parenthesized_arithmetic() { assert_eq!(out, "20"); } + /// Verifies a complex expression mixing parentheses, addition, multiplication, and subtraction: (10 + 5) * 2 - 7 = 23. #[test] fn test_complex_expression() { @@ -67,6 +73,7 @@ fn test_complex_expression() { assert_eq!(out, "23"); } + /// Verifies assignment of an arithmetic expression result to a variable, then echo: $a + $b → $c → output. #[test] fn test_arithmetic_assign_and_echo() { @@ -74,6 +81,7 @@ fn test_arithmetic_assign_and_echo() { assert_eq!(out, "42"); } + /// Verifies subtraction producing a negative result: 3 - 10 = -7, confirming signed integer handling. #[test] fn test_subtraction_negative_result() { @@ -81,6 +89,7 @@ fn test_subtraction_negative_result() { assert_eq!(out, "-7"); } + /// Verifies left-associative chaining of addition: 1 + 2 + 3 + 4 = 10. #[test] fn test_nested_arithmetic() { @@ -88,6 +97,7 @@ fn test_nested_arithmetic() { assert_eq!(out, "10"); } + /// Verifies that adding 1 to the maximum 64-bit integer constant overflows to float at compile time. #[test] fn test_constant_int_add_overflow_promotes_to_float() { @@ -95,6 +105,7 @@ fn test_constant_int_add_overflow_promotes_to_float() { assert_eq!(out, "double"); } + /// Verifies that squaring a large integer constant overflows to float at compile time. #[test] fn test_constant_int_multiply_overflow_promotes_to_float() { @@ -102,6 +113,7 @@ fn test_constant_int_multiply_overflow_promotes_to_float() { assert_eq!(out, "double"); } + /// Verifies that adding 1 to the maximum 64-bit integer at runtime overflows to float. #[test] fn test_runtime_int_add_overflow_promotes_to_float() { @@ -109,6 +121,7 @@ fn test_runtime_int_add_overflow_promotes_to_float() { assert_eq!(out, "double"); } + /// Verifies that subtracting past the minimum 64-bit integer at runtime overflows to float. #[test] fn test_runtime_int_sub_overflow_promotes_to_float() { @@ -116,6 +129,7 @@ fn test_runtime_int_sub_overflow_promotes_to_float() { assert_eq!(out, "double"); } + /// Verifies that squaring a large integer at runtime overflows to float. #[test] fn test_runtime_int_multiply_overflow_promotes_to_float() { @@ -123,6 +137,7 @@ fn test_runtime_int_multiply_overflow_promotes_to_float() { assert_eq!(out, "double"); } + /// Verifies that runtime integer arithmetic without overflow remains integer, not float. #[test] fn test_runtime_int_arithmetic_without_overflow_stays_integer() { @@ -130,6 +145,7 @@ fn test_runtime_int_arithmetic_without_overflow_stays_integer() { assert_eq!(out, "integer:42"); } + /// Verifies that a runtime overflow result (float) participates correctly in subsequent arithmetic. #[test] fn test_runtime_overflow_result_participates_in_later_arithmetic() { @@ -137,6 +153,7 @@ fn test_runtime_overflow_result_participates_in_later_arithmetic() { assert_eq!(out, "double"); } + /// Verifies that pre-increment promotes an overflowing int local and returns the promoted value. #[test] fn test_runtime_pre_increment_overflow_promotes_local_to_float() { @@ -144,6 +161,7 @@ fn test_runtime_pre_increment_overflow_promotes_local_to_float() { assert_eq!(out, "double:double"); } + /// Verifies that post-increment returns the old int while promoting the local for later reads. #[test] fn test_runtime_post_increment_overflow_returns_old_int_and_promotes_local() { @@ -151,7 +169,6 @@ fn test_runtime_post_increment_overflow_returns_old_int_and_promotes_local() { assert_eq!(out, "integer:double"); } -// --- Phase 3: Concatenation --- /// Verifies string literal concatenation: "Hello, " . "World!" = "Hello, World!". #[test] @@ -160,6 +177,7 @@ fn test_concat_literals() { assert_eq!(out, "Hello, World!"); } + /// Verifies string concatenation with variables: loads two strings from memory and concatenates. #[test] fn test_concat_variables() { @@ -167,6 +185,7 @@ fn test_concat_variables() { assert_eq!(out, "Hello, World!"); } + /// Verifies left-associative chaining of string concatenation: "a" . "b" . "c" = "abc". #[test] fn test_concat_chain() { @@ -174,6 +193,7 @@ fn test_concat_chain() { assert_eq!(out, "abc"); } + /// Verifies concatenation assignment: $msg = "foo" . "bar"; echo $msg; = "foobar". #[test] fn test_concat_assign() { @@ -181,6 +201,7 @@ fn test_concat_assign() { assert_eq!(out, "foobar"); } + /// Verifies concatenation with embedded newline escape: "hello" . "\n" outputs "hello\n". #[test] fn test_concat_with_newline() { @@ -188,6 +209,7 @@ fn test_concat_with_newline() { assert_eq!(out, "hello\n"); } + /// Verifies that concatenating an array onto a string stringifies the array to the literal /// "Array" (matching PHP's array-to-string conversion) for both an array literal and an /// array-typed function result, instead of crashing by treating the array pointer as a string. @@ -204,6 +226,7 @@ echo "prefix" . makeArr(); assert_eq!(out, "aArray|prefixArray"); } + /// Verifies that echoing an array stringifies to the literal "Array" (matching PHP), routing /// through the same string-coercion path as concatenation. #[test] @@ -212,6 +235,7 @@ fn test_echo_array_stringifies_to_array_literal() { assert_eq!(out, "Array"); } + /// Verifies that interpolating an array into a double-quoted string stringifies it to the /// literal "Array" (matching PHP) for both simple `$a` and complex `{$a}` interpolation. #[test] @@ -220,7 +244,6 @@ fn test_interpolated_array_stringifies_to_array_literal() { assert_eq!(out, "v=Array|w=Array"); } -// --- Phase 3: Mixed-type concatenation --- /// Verifies concatenation of string literal and integer literal: "Value: " . 42 = "Value: 42". #[test] @@ -229,6 +252,7 @@ fn test_concat_string_and_int() { assert_eq!(out, "Value: 42"); } + /// Verifies concatenation of integer literal and string literal: 42 . " is the answer" = "42 is the answer". #[test] fn test_concat_int_and_string() { @@ -236,6 +260,7 @@ fn test_concat_int_and_string() { assert_eq!(out, "42 is the answer"); } + /// Verifies concatenation of two integer literals coerces to string: 1 . 2 = "12". #[test] fn test_concat_int_and_int() { @@ -243,6 +268,7 @@ fn test_concat_int_and_int() { assert_eq!(out, "12"); } + /// Verifies concatenation of a string literal and a parenthesized expression result: "Result: " . ($a + $b) = "Result: 42". #[test] fn test_concat_expr_result() { @@ -250,6 +276,7 @@ fn test_concat_expr_result() { assert_eq!(out, "Result: 42"); } + /// Verifies mixed-type concatenation chaining left-to-right: "x=" . 5 . " y=" . 10 = "x=5 y=10". #[test] fn test_concat_chain_mixed() { @@ -257,6 +284,7 @@ fn test_concat_chain_mixed() { assert_eq!(out, "x=5 y=10"); } + /// Verifies concatenation with a negative integer: "num: " . -7 = "num: -7". #[test] fn test_concat_negative_int() { @@ -264,7 +292,6 @@ fn test_concat_negative_int() { assert_eq!(out, "num: -7"); } -// --- Modulo --- /// Verifies integer modulo: 10 % 3 = 1. #[test] @@ -273,6 +300,7 @@ fn test_modulo() { assert_eq!(out, "1"); } + /// Verifies modulo with zero remainder: 15 % 5 = 0. #[test] fn test_modulo_zero_remainder() { @@ -280,7 +308,6 @@ fn test_modulo_zero_remainder() { assert_eq!(out, "0"); } -// --- Comparison operators --- /// Verifies loose equality comparison returning true: 1 == 1 outputs "1". #[test] @@ -289,6 +316,7 @@ fn test_equal_true() { assert_eq!(out, "1"); } + /// Verifies loose equality comparison returning false: 1 == 2 outputs empty string (echo false prints nothing in PHP). #[test] fn test_equal_false() { @@ -296,6 +324,7 @@ fn test_equal_false() { assert_eq!(out, ""); // echo false prints nothing in PHP } + /// Verifies loose inequality returning true: 1 != 2 outputs "1". #[test] fn test_not_equal() { @@ -303,7 +332,6 @@ fn test_not_equal() { assert_eq!(out, "1"); } -// --- Loose comparison across types --- /// Verifies loose equality at compile time: empty string equals false, var_dump shows bool(true). #[test] @@ -312,6 +340,7 @@ fn test_loose_eq_empty_string_false() { assert_eq!(out, "bool(true)\n"); } + /// Verifies loose equality at compile time: integer 0 equals false, var_dump shows bool(true). #[test] fn test_loose_eq_zero_false() { @@ -319,6 +348,7 @@ fn test_loose_eq_zero_false() { assert_eq!(out, "bool(true)\n"); } + /// Verifies loose equality at compile time: integer 1 equals true, var_dump shows bool(true). #[test] fn test_loose_eq_one_true() { @@ -326,6 +356,7 @@ fn test_loose_eq_one_true() { assert_eq!(out, "bool(true)\n"); } + /// Verifies loose equality at compile time: string "0" equals false (string zero is falsy), var_dump shows bool(true). #[test] fn test_loose_eq_string_vs_int() { @@ -333,6 +364,7 @@ fn test_loose_eq_string_vs_int() { assert_eq!(out, "bool(true)\n"); } + /// Verifies loose inequality at compile time: empty string is not equal to true, var_dump shows bool(true). #[test] fn test_loose_neq_empty_string_true() { @@ -340,6 +372,7 @@ fn test_loose_neq_empty_string_true() { assert_eq!(out, "bool(true)\n"); } + /// Verifies loose equality at compile time: null equals false (null is falsy), var_dump shows bool(true). #[test] fn test_loose_eq_null_false() { @@ -347,6 +380,7 @@ fn test_loose_eq_null_false() { assert_eq!(out, "bool(true)\n"); } + /// Verifies compile-time loose equality of two non-numeric strings compares by byte sequence, not lexicographically. #[test] fn test_constant_loose_eq_non_numeric_strings_compare_by_bytes() { @@ -354,6 +388,7 @@ fn test_constant_loose_eq_non_numeric_strings_compare_by_bytes() { assert_eq!(out, "bool(false)\n"); } + /// Verifies compile-time loose equality of numeric strings ("0" == "00") compares numerically as equal. #[test] fn test_constant_loose_eq_numeric_strings_compare_numerically() { @@ -361,6 +396,7 @@ fn test_constant_loose_eq_numeric_strings_compare_numerically() { assert_eq!(out, "bool(true)\n"); } + /// Verifies compile-time loose equality of number and non-numeric string is false: 0 == "abc" is bool(false). #[test] fn test_constant_loose_eq_number_and_non_numeric_string_is_false() { @@ -368,6 +404,7 @@ fn test_constant_loose_eq_number_and_non_numeric_string_is_false() { assert_eq!(out, "bool(false)\n"); } + /// Verifies compile-time loose equality of number and numeric string is true: 10 == "1e1" both evaluate to 10.0. #[test] fn test_constant_loose_eq_number_and_numeric_string_is_true() { @@ -375,6 +412,7 @@ fn test_constant_loose_eq_number_and_numeric_string_is_true() { assert_eq!(out, "bool(true)\n"); } + /// Verifies runtime float comparisons against NaN match PHP: NaN is uncomparable, so `<`, `<=`, /// `>`, `>=`, `==` are all false and `!=` is true, while `<=>` yields 1 in every direction /// (including NaN<=>NaN). Operands come from `float`-returning calls so the optimizer cannot @@ -402,6 +440,7 @@ echo ($nan <=> $one), ($one <=> $nan), ($nan <=> $nan); ); } + /// Verifies runtime loose equality of two non-numeric strings compares by byte sequence. #[test] fn test_runtime_loose_eq_non_numeric_strings_compare_by_bytes() { @@ -409,6 +448,7 @@ fn test_runtime_loose_eq_non_numeric_strings_compare_by_bytes() { assert_eq!(out, "bool(false)\n"); } + /// Verifies runtime loose equality of numeric strings "0" == "00" compares numerically as equal. #[test] fn test_runtime_loose_eq_numeric_strings_compare_numerically() { @@ -416,6 +456,7 @@ fn test_runtime_loose_eq_numeric_strings_compare_numerically() { assert_eq!(out, "bool(true)\n"); } + /// Verifies runtime loose equality of number and non-numeric string is false: $n=0, $s="abc" → bool(false). #[test] fn test_runtime_loose_eq_number_and_non_numeric_string_is_false() { @@ -423,6 +464,7 @@ fn test_runtime_loose_eq_number_and_non_numeric_string_is_false() { assert_eq!(out, "bool(false)\n"); } + /// Verifies runtime loose equality of number and numeric string is true: $n=10, $s="1e1" → bool(true). #[test] fn test_runtime_loose_eq_number_and_numeric_string_is_true() { @@ -430,6 +472,7 @@ fn test_runtime_loose_eq_number_and_numeric_string_is_true() { assert_eq!(out, "bool(true)\n"); } + /// Verifies runtime loose equality of bool and string uses truthiness: true=="abc" is true (truthy), false=="abc" is false. #[test] fn test_runtime_loose_eq_bool_and_string_uses_truthiness() { @@ -437,6 +480,7 @@ fn test_runtime_loose_eq_bool_and_string_uses_truthiness() { assert_eq!(out, "bool(true)\nbool(false)\n"); } + /// Verifies runtime loose equality of null and string uses empty-string rule: null=="" is true, null=="0" is false. #[test] fn test_runtime_loose_eq_null_and_string_uses_empty_string_rule() { @@ -444,6 +488,7 @@ fn test_runtime_loose_eq_null_and_string_uses_empty_string_rule() { assert_eq!(out, "bool(true)\nbool(false)\n"); } + /// Verifies integer less-than comparison: 1 < 2 outputs "1". #[test] fn test_less_than() { @@ -451,6 +496,7 @@ fn test_less_than() { assert_eq!(out, "1"); } + /// Verifies integer greater-than comparison: 2 > 1 outputs "1". #[test] fn test_greater_than() { @@ -458,6 +504,7 @@ fn test_greater_than() { assert_eq!(out, "1"); } + /// Verifies integer less-than-or-equal comparison: 2 <= 2 outputs "1". #[test] fn test_less_equal() { @@ -465,6 +512,7 @@ fn test_less_equal() { assert_eq!(out, "1"); } + /// Verifies integer greater-than-or-equal comparison: 1 >= 2 outputs empty string (false). #[test] fn test_greater_equal() { @@ -472,6 +520,7 @@ fn test_greater_equal() { assert_eq!(out, ""); } + /// Regression: a loose `==` between a plain integer and a boxed `Mixed` integer must hold in both /// operand orders. Loading a Mixed operand unboxes it through a runtime call that clobbers the /// scratch registers; without saving the already-loaded left operand, `Int == Mixed` lost its left @@ -491,6 +540,7 @@ echo ($i == $m ? "y" : "n"), ($m == $i ? "y" : "n"), ($i == $h["n"] ? "y" : "n") assert_eq!(out, "yyyn"); } + /// Regression for #397: loose equality with a Mixed operand holding a float /// must not truncate the float to int before comparison. `1.5 == 1` must be /// false, `1.5 == 1.5` must be true. @@ -509,6 +559,7 @@ check(1.5); assert_eq!(out, "bool(false)\nbool(true)\nbool(false)\n"); } + /// Regression for #397: switch with a Mixed subject holding a float must use /// loose equality, not integer truncation. `switch(1.5) { case 1: ...; case /// 1.5: ... }` must match `case 1.5`. @@ -529,6 +580,7 @@ echo classify(1.5), "\n"; assert_eq!(out, "onefive\n"); } + /// Regression for #397: switch with a Mixed subject holding an int must still /// match int cases correctly (no regression from the Mixed routing change). #[test] @@ -549,6 +601,7 @@ echo classify(2), "\n"; assert_eq!(out, "int-one\nother\n"); } + /// Regression for #397: `!=` (LooseNotEq) with a Mixed float operand must /// also avoid truncation. `1.5 != 1` must be true. #[test] @@ -565,6 +618,7 @@ check(1.5); assert_eq!(out, "bool(true)\nbool(false)\n"); } + /// Regression: loose equality with a Mixed NaN payload must preserve PHP's /// unordered-float rule. `NAN == 1` is false and `NAN != 1` is true, including /// on x86_64 where unordered `ucomisd` comparisons set ZF. @@ -582,6 +636,7 @@ check(NAN); assert_eq!(out, "bool(false)\nbool(true)\n"); } + /// Regression: loose equality with a Mixed string payload must use PHP /// numeric-string rules instead of `atof`-style casts. Non-numeric strings are /// not equal to numbers, while numeric strings compare by parsed numeric value. @@ -605,6 +660,7 @@ check("1.5"); ); } + /// Regression: loose equality between a Mixed boolean and a number compares by /// PHP truthiness, not by comparing `true` as `1.0`. #[test] @@ -630,6 +686,7 @@ check_false(false); ); } + /// Regression: Mixed array payloads are not loosely equal to numeric operands. #[test] fn test_loose_eq_mixed_array_vs_number_is_false() { @@ -646,7 +703,6 @@ check([1]); assert_eq!(out, "bool(false)\nbool(true)\nbool(false)\nbool(true)\n"); } -// --- Deep array strict equality (`===` / `!==`) --- /// Regression: two empty array literals are strictly equal (deep structural `===`, not pointer /// identity). This is the base case of the `__rt_array_strict_eq` runtime helper. @@ -661,6 +717,7 @@ var_dump([] === [1]); assert_eq!(out, "bool(true)\nbool(false)\n"); } + /// Regression: a value typed `array|false` (a runtime-Mixed union) compared against an array /// literal must deep-compare, not compare heap pointers. Previously `$x === []` was always false. #[test] @@ -685,6 +742,7 @@ var_dump($no === []); ); } + /// Regression: indexed integer arrays compare element-by-element with length sensitivity. #[test] fn test_strict_eq_indexed_int_arrays() { @@ -699,6 +757,7 @@ var_dump([1, 2] === [1, 2, 3]); assert_eq!(out, "bool(true)\nbool(false)\nbool(false)\nbool(false)\n"); } + /// Regression: string-element arrays compare by string contents, not pointer identity. #[test] fn test_strict_eq_string_element_arrays() { @@ -712,6 +771,7 @@ var_dump(["a", "b"] === ["a"]); assert_eq!(out, "bool(true)\nbool(false)\nbool(false)\n"); } + /// Regression: associative arrays require the same key => value pairs in the same insertion order. #[test] fn test_strict_eq_assoc_arrays_order_sensitive() { @@ -726,6 +786,7 @@ var_dump(["x" => 1, "y" => 2] === ["y" => 2, "x" => 1]); assert_eq!(out, "bool(true)\nbool(false)\nbool(false)\nbool(false)\n"); } + /// Regression: nested arrays compare recursively through `__rt_mixed_strict_eq` re-entering /// `__rt_array_strict_eq`. #[test] @@ -741,6 +802,7 @@ var_dump([["a" => [1]]] === [["a" => [2]]]); assert_eq!(out, "bool(true)\nbool(false)\nbool(true)\nbool(false)\n"); } + /// Regression: heterogeneous arrays (mixed element types, stored as boxed Mixed slots) compare /// with full per-element type precision. #[test] @@ -755,6 +817,7 @@ var_dump([1, "a", 3] === [1, "a", 4]); assert_eq!(out, "bool(true)\nbool(false)\nbool(false)\n"); } + /// Regression: `!==` is the negation of the deep `===` for arrays. #[test] fn test_strict_not_eq_arrays() { @@ -767,3 +830,485 @@ var_dump(["a" => 1] !== ["a" => 1]); ); assert_eq!(out, "bool(true)\nbool(false)\nbool(false)\n"); } + +/// Verifies a single `.` chain whose result exceeds the shared 64 KiB concat scratch buffer keeps +/// every byte instead of writing past the scratch end into the adjacent BSS globals. This exact +/// program used to segfault before `__rt_concat` reserved bounded destination storage. +#[test] +fn test_concat_result_larger_than_concat_scratch() { + let out = compile_and_run( + r#"` behaves exactly like `!=` at runtime, including the loose numeric-string +/// comparison and array comparison cases. Expected output matches `php -r` on 8.4. +#[test] +fn test_angle_not_equal_matches_not_equal() { + let out = compile_and_run( + r#" 2); +var_dump(1 <> 1); +var_dump("1" <> 1); +var_dump(1 + 1 <> 2); +var_dump(true <> false); +var_dump([1, 2] <> [1, 3]); +$a = 5; +var_dump($a <> 5, $a <> 6); +"#, + ); + assert_eq!( + out, + "bool(true)\nbool(false)\nbool(false)\nbool(false)\nbool(true)\nbool(true)\nbool(false)\nbool(true)\n" + ); +} + + +/// Verifies `< >` separated by whitespace is still two relational operators, so the +/// `<>` token does not swallow chained comparisons such as `(1 < 2) > 0`. +#[test] +fn test_angle_not_equal_does_not_capture_spaced_comparisons() { + let out = compile_and_run(" 0);"); + assert_eq!(out, "bool(true)\n"); +} + + +/// Verifies `<<` with a shift count of 64 or more yields `0` instead of masking the count. +/// +/// Raw AArch64 `lsl` / x86_64 `shl` by register mask the count to six bits, so `1 << 64` used +/// to produce `1` and `1 << 100` produced `68719476736`. Operands go through `$argc` so the +/// constant folders cannot evaluate the shift at compile time. +#[test] +fn test_shift_left_count_at_or_above_word_size_is_zero() { + let out = compile_and_run( + r#">` with a shift count of 64 or more saturates to a full sign fill like PHP. +/// +/// PHP yields `0` for a non-negative value and `-1` for a negative one; the masked hardware +/// shift produced whatever `count % 64` happened to give. +#[test] +fn test_shift_right_count_at_or_above_word_size_saturates_to_sign() { + let out = compile_and_run( + r#"> (64 * $n)); +var_dump((-8 * $n) >> (64 * $n)); +var_dump((-8 * $n) >> (100 * $n)); +var_dump((-1 * $n) >> (64 * $n)); +var_dump(PHP_INT_MIN >> (65 * $n)); +"#, + ); + assert_eq!(out, "int(0)\nint(-1)\nint(-1)\nint(-1)\nint(-1)\n"); +} + + +/// Verifies ordinary in-range shift counts are unaffected by the PHP shift guards. +#[test] +fn test_shift_in_range_counts_are_unchanged() { + let out = compile_and_run( + r#"> (2 * $n); +$a = 5; $a <<= (2 * $n); +$b = 40; $b >>= (3 * $n); +echo "|", $a, "|", $b; +"#, + ); + assert_eq!(out, "1|8|-2|20|5"); +} + + +/// Verifies `PHP_INT_MIN % -1` evaluates to `0` on both supported targets. +/// +/// x86_64 `idiv` raises `#DE` (SIGFPE) for that operand pair, so the lowering must answer the +/// `-1` divisor without reaching the divide unit; AArch64's `sdiv`/`msub` already wraps to `0`. +#[test] +fn test_int_min_modulo_negative_one_is_zero() { + let out = compile_and_run( + r#"1,"b"=>2] == ["b"=>2,"a"=>1]` is +/// true while `[1,2] == [2=>1,3=>2]` is false. `["a"=>null] == ["b"=>null]` pins +/// that a MISSING key never matches a stored `null`. +#[test] +fn test_loose_equality_array_versus_array() { + let out = compile_and_run( + r#" $n, "b" => 2] == ["b" => 2, "a" => $n]); +var_dump([1, 2] == [2 => 1, 3 => 2]); +var_dump([[1, $n], [3]] == [[1, $n], [3]], [[1, $n], [3]] == [[1, $n], [4]]); +var_dump([null] == [0 * $n], ["a" => null] == ["b" => null]); +"#, + ); + assert_eq!( + out, + "bool(true)\nbool(false)\nbool(true)\nbool(false)\n\ + bool(true)\nbool(false)\n\ + bool(true)\nbool(false)\n\ + bool(true)\nbool(false)\n" + ); +} + + +/// Verifies statement-position `++`/`--` on `$this` members, in both prefix and postfix +/// spelling, over an int property, an array-element property, and a float property. +/// Regression for the parser accepting `$obj->n++` but rejecting `$this->n++`. +#[test] +fn test_incdec_on_this_members() { + let out = compile_and_run( + r#"n++; } + public function bumpPre(): void { ++$this->n; } + public function dropPost(): void { $this->n--; } + public function dropPre(): void { --$this->n; } + public function bumpElem(): void { $this->arr[0]++; } + public function bumpElemPre(): void { ++$this->arr[1]; } + public function bumpFloat(): void { $this->f++; } +} +$c = new C(); +$c->bumpPost(); $c->bumpPre(); $c->bumpPost(); +echo $c->n, "|"; +$c->dropPost(); $c->dropPre(); +echo $c->n, "|"; +$c->bumpElem(); $c->bumpElemPre(); +echo $c->arr[0], ",", $c->arr[1], "|"; +$c->bumpFloat(); +echo $c->f; +"#, + ); + assert_eq!(out, "3|1|2,3|2.5"); +} + + +/// Verifies prefix `++`/`--` in statement position on the complex targets that previously +/// only worked in postfix spelling: object properties and array elements. +#[test] +fn test_prefix_incdec_on_complex_targets() { + let out = compile_and_run( + r#"n; ++$c->n; --$c->n; ++$c->arr[0]; +echo $c->n, ",", $c->arr[0], "|"; +$a = [1, 2]; +++$a[0]; --$a[1]; +echo $a[0], ",", $a[1], "|"; +$x = 1; ++$x; $x++; +echo $x; +"#, + ); + assert_eq!(out, "1,2|2,1|3"); +} + + +/// Verifies `$this` member increments still work inside nested control flow, where the +/// statement parser is re-entered from a loop or conditional body. +#[test] +fn test_incdec_on_this_members_inside_control_flow() { + let out = compile_and_run( + r#"n++; } } +} +$c = new C(); +$c->run(); +echo $c->n, "|"; +while ($c->n < 5) { $c->n++; } +echo $c->n; +"#, + ); + assert_eq!(out, "3|5"); +} + + +/// Verifies `++`/`--` on float locals in every spelling, including the value each form +/// returns and the IEEE edge cases. Expected output matches `php -r` on 8.4. +#[test] +fn test_incdec_on_float_locals() { + let out = compile_and_run( + r#" 99 ? 1 : "az"; +$m++; +var_dump($m); +"#, + ); + assert_eq!( + out, + "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD\n\ + ba AAa b0\nstring(2) \"ba\"\n" + ); +} + + +/// Verifies the int/float boundary of a numeric-string increment: a value that still fits +/// stays an `int`, `PHP_INT_MAX` promotes to `float`, a 20-digit string is already a float, +/// and decrementing `PHP_INT_MAX` stays an exact `int`. +#[test] +fn test_numeric_string_increment_int_boundary() { + let out = compile_and_run( + r#"` on PHP 8.4.20 before the test was +//! written; a fold that disagrees with these strings is a silent miscompilation. +//! - Operands are literals on purpose — the fold is the thing under test. Where a construct has +//! to survive folding, `$argc` supplies a runtime-unknown value. + +use super::*; + +/// Verifies integer comparisons above 2^53 stay exact instead of routing through `f64`. +/// +/// php -r 'var_dump(9223372036854775806 < 9223372036854775807, 9223372036854775806 <=> 9223372036854775807, 9223372036854775806 == 9223372036854775807);' +#[test] +fn test_fold_large_integer_comparisons_match_php() { + let out = compile_and_run( + r#" 9223372036854775807); +var_dump(9223372036854775806 == 9223372036854775807); +var_dump(9223372036854775806 > 9223372036854775807); +"#, + ); + assert_eq!(out, "bool(true)\nint(-1)\nbool(false)\nbool(false)\n"); +} + +/// Verifies an integer compared against a numeric string uses PHP 8's exact integer rule. +/// +/// php -r 'var_dump(9223372036854775806 == "9223372036854775807", 9223372036854775807 == "9223372036854775807", "9223372036854775806" == "9223372036854775807");' +#[test] +fn test_fold_integer_versus_numeric_string_matches_php() { + let out = compile_and_run( + r#" "b", "a" < "b", "B" < "a", "10" > "9", "abc" == "abc");' +/// +/// These compile only because the fold answers them before type checking: the checker still +/// rejects `<`, `<=`, `>`, `>=` and `<=>` on non-constant string operands (issue #507, pinned +/// by `error_tests::misc::syntax_misc::test_error_spaceship_string`). The fold is the +/// PHP-correct half of that gap, so it must not regress while the checker catches up. +#[test] +fn test_fold_literal_string_comparisons_match_php() { + let out = compile_and_run( + r#" "b"); +var_dump("a" < "b"); +var_dump("B" < "a"); +var_dump("10" > "9"); +var_dump("abc" == "abc"); +"#, + ); + assert_eq!( + out, + "int(-1)\nbool(true)\nbool(true)\nbool(true)\nbool(true)\n" + ); +} + +/// Verifies `switch` case selection over a constant subject uses PHP's `==`. +/// +/// `switch (2) { case true: }` selects the case because `2 == true` is `(bool) 2`; the fold +/// used to coerce `true` to the integer `1` and fall through to `default`. +#[test] +fn test_fold_switch_case_loose_comparison_matches_php() { + let out = compile_and_run( + r#"> 64); +var_dump(-1 >> 63); +"#, + ); + assert_eq!( + out, + concat!( + "int(0)\n", + "int(2)\n", + "float(3.5)\n", + "int(8)\n", + "float(0.5)\n", + "int(-9223372036854775808)\n", + "int(0)\n", + "int(-1)\n", + "int(-1)\n", + ) + ); +} + +/// Verifies `intdiv(PHP_INT_MIN, -1)` still raises instead of being folded. +/// +/// php -r 'var_dump(intdiv(PHP_INT_MIN, -1));' throws `ArithmeticError`, so the compiled +/// program must fail rather than print a wrapped integer. +#[test] +fn test_intdiv_int_min_by_minus_one_still_raises() { + let output = compile_and_run_expect_failure( + " "a", false => "b"][0], ["1" => "a", 1 => "b"]["1"], [null => "a", "" => "b"][null]);' +#[test] +fn test_fold_assoc_array_key_normalization_matches_php() { + let out = compile_and_run( + r#" "a", false => "b"][0]); +var_dump(["1" => "a", 1 => "b"]["1"]); +var_dump([null => "a", "" => "b"][null]); +var_dump([true => "a", 1 => "b"][true]); +var_dump(["01" => "a", 1 => "b"]["01"]); +var_dump([" 1" => "a", 1 => "b"][" 1"]); +var_dump([false => "a", true => "b", null => "c"][0]); +var_dump([false => "a", true => "b", null => "c"][1]); +var_dump([false => "a", true => "b", null => "c"][""]); +"#, + ); + assert_eq!( + out, + concat!( + "string(1) \"b\"\n", + "string(1) \"b\"\n", + "string(1) \"b\"\n", + "string(1) \"b\"\n", + "string(1) \"a\"\n", + "string(1) \"a\"\n", + "string(1) \"a\"\n", + "string(1) \"b\"\n", + "string(1) \"c\"\n", + ) + ); +} + +/// Verifies constant propagation keeps `-0.0` distinct from `0.0`. +/// +/// php -r '$c = $argc > 1000; $x = $c ? 0.0 : -0.0; echo $x;' prints `-0`; merging the ternary +/// arms into one constant printed `0`. +#[test] +fn test_signed_zero_survives_constant_propagation() { + let out = compile_and_run( + r#" 1000; +$x = $c ? 0.0 : -0.0; +echo $x, "\n"; +var_dump($x); +"#, + ); + assert_eq!(out, "-0\nfloat(-0)\n"); +} + +/// Verifies DCE guard state treats `0.0` and `-0.0` as the same value under `===`. +/// +/// php -r 'function probe(float $x): void { if ($x === 0.0) { if (-0.0 === $x) { echo "A"; } else { echo "B"; } } } probe($argc > 1000 ? 1.0 : -0.0);' prints `A`. +#[test] +fn test_signed_zero_guard_does_not_prune_live_branch() { + let out = compile_and_run( + r#" 1000 ? 1.0 : -0.0); +"#, + ); + assert_eq!(out, "A"); +} + +/// Verifies `(float)` and `(int)` casts of literal strings use PHP's numeric grammar. +/// +/// PHP has no `INF`/`NAN`/hexadecimal numeric-string forms, so `(float) "INF"` is `0`; the fold +/// used Rust's `str::parse::()`, which accepted them. +#[test] +fn test_fold_string_casts_match_php() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } +} +echo count(array_chunk($a, 2)), "\n"; +"#, + ); + assert_eq!( + out, + "ValueError|array_chunk(): Argument #2 ($length) must be greater than 0\n\ + ValueError|array_chunk(): Argument #2 ($length) must be greater than 0\n\ + 2\n" + ); +} + +/// Regression: an uncaught non-positive `array_chunk()` length reports PHP's uncaught-`ValueError` +/// fatal instead of `Fatal error: heap memory exhausted`. +#[test] +fn test_array_chunk_zero_length_uncaught_reports_value_error_fatal() { + let err = compile_and_run_expect_failure(r#"getMessage(); +} $ok = array_fill(0, 3, "ab"); echo "|", count($ok), ":", $ok[0], $ok[2]; "#, ); - assert_eq!(out, "arr:0|3:abab"); + assert_eq!( + out, + "ValueError|array_fill(): Argument #2 ($count) must be greater than or equal to 0|3:abab" + ); } /// Regression (issue #407): reading a typed `array` property by a *variable* string key must diff --git a/tests/codegen/regressions/scalars_and_regex.rs b/tests/codegen/regressions/scalars_and_regex.rs index 7dd9526084..fc22169910 100644 --- a/tests/codegen/regressions/scalars_and_regex.rs +++ b/tests/codegen/regressions/scalars_and_regex.rs @@ -175,12 +175,32 @@ fn test_modulo_normal() { assert_eq!(out, "0"); } -/// Verifies that modulo by zero returns 0 (no crash). -/// Regression for issue #23 (modulo by zero). +/// Verifies that an uncaught modulo by zero is a `DivisionByZeroError` fatal, not the value `0`. +/// +/// Regression for issue #23 (modulo by zero). Reference PHP 8.4 raises +/// `DivisionByZeroError: Modulo by zero`; elephc used to fall back to `mov result, 0` and hand +/// back `0`. With no handler active the program still terminates — the diagnostic just names +/// the PHP class now, matching `Fatal error: Uncaught DivisionByZeroError: Modulo by zero`. #[test] -fn test_modulo_by_zero() { - let out = compile_and_run("getMessage(); }", + ); + assert_eq!(out, "DivisionByZeroError:Modulo by zero"); } /// Verifies normal modulo remainder: `7 % 3` returns 1. diff --git a/tests/codegen/regressions/symbol_collisions.rs b/tests/codegen/regressions/symbol_collisions.rs new file mode 100644 index 0000000000..1c591cdae8 --- /dev/null +++ b/tests/codegen/regressions/symbol_collisions.rs @@ -0,0 +1,128 @@ +//! Purpose: +//! Regression tests for generated-symbol and assembly-label collisions between unrelated PHP +//! declarations whose names differ only in where an underscore or a non-ASCII character falls. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every fixture used to either miscompile (two declarations sharing one storage cell) or fail +//! to assemble with a duplicate-symbol error, so "compiles and matches PHP" is the assertion. +//! - Expected outputs are PHP 8.4 reference outputs; the fixtures avoid runtime-unknown values so +//! they stay readable, and none of them are constant-foldable across calls. + +use crate::support::*; + +/// Verifies two classes whose static properties share a mangled `_` spelling +/// keep separate storage. `a::$u_b` and `a_u::$b` both mangled to `_static_prop_a_u_u_b`, so the +/// duplicate `.comm` directives merged and both reads observed the last writer. +#[test] +fn test_static_properties_of_underscore_ambiguous_classes_do_not_share_storage() { + let out = compile_and_run( + r#"_` spellings coincide still +/// assemble. `a::u_b()` and `a_u::b()` both produced `_method_a_u_u_b`, so valid PHP failed to +/// compile with an `already defined` assembler error. +#[test] +fn test_methods_of_underscore_ambiguous_classes_compile_and_dispatch() { + let out = compile_and_run( + r#"u_b(); +$y = new a_u(); $y->b(); +"#, + ); + assert_eq!(out, "AB"); +} + +/// Verifies static methods and enum cases survive the same ambiguous class/member join that broke +/// instance methods. +#[test] +fn test_static_methods_and_enum_cases_of_underscore_ambiguous_names_compile() { + let out = compile_and_run( + r#"name, e_u::c->name; +"#, + ); + assert_eq!(out, "STu_cc"); +} + +/// Verifies a static local named `$x_init` no longer aliases the one-shot initialization flag of +/// static `$x`. The flag used to be `_init` built from the raw PHP variable name, +/// so writing `$x_init = 0` cleared `$x`'s flag and re-ran its initializer on the next call. +#[test] +fn test_static_local_named_init_does_not_alias_another_statics_flag() { + let out = compile_and_run( + r#" 0) { echo "p"; } else { echo "n"; } } +function aéb($n) { if ($n > 0) { echo "P"; } else { echo "N"; } } +a_b(1); aéb(1); a_b(-1); aéb(-1); +"#, + ); + assert_eq!(out, "pPnN"); +} diff --git a/tests/codegen/runtime_gc.rs b/tests/codegen/runtime_gc.rs index 72a5140c56..db3a7e09e8 100644 --- a/tests/codegen/runtime_gc.rs +++ b/tests/codegen/runtime_gc.rs @@ -5,7 +5,7 @@ //! - `cargo test` through Rust's test harness. //! //! Key details: -//! - Submodules group focused fixtures for basics, regressions, stack args, copy-on-write and cycle handling, growth, related suites, and resource scope-cleanup. +//! - Submodules group focused fixtures for basics, regressions, stack args, copy-on-write and cycle handling, growth, related suites, resource scope-cleanup, by-reference builtin arguments that name a property, static property, or container element, and the reference a `foreach` loop holds on an object source. #[path = "runtime_gc/basics.rs"] mod basics; @@ -17,6 +17,10 @@ mod parse_url; mod regressions; #[path = "runtime_gc/assoc_rebind_release.rs"] mod assoc_rebind_release; +#[path = "runtime_gc/by_ref_place_args.rs"] +mod by_ref_place_args; +#[path = "runtime_gc/foreach_object_source.rs"] +mod foreach_object_source; #[path = "runtime_gc/stack_args.rs"] mod stack_args; #[path = "runtime_gc/cow_and_cycles.rs"] diff --git a/tests/codegen/runtime_gc/by_ref_place_args.rs b/tests/codegen/runtime_gc/by_ref_place_args.rs new file mode 100644 index 0000000000..f49f904efc --- /dev/null +++ b/tests/codegen/runtime_gc/by_ref_place_args.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Heap-debug coverage for mutating array builtins whose by-reference argument is a property, +//! static property, or container element. Those calls are lowered as +//! `$tmp = ; f($tmp, ...); = $tmp;`, which adds a synthetic local, a +//! copy-on-write separation, and a write-back that releases the property's previous occupant. +//! Every one of those steps has to stay balanced. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Each fixture runs under `--heap-debug` and asserts `leak summary: clean`, so an +//! unreleased separated copy or an unreleased synthetic local shows up as a leak. +//! - The aliased fixtures also assert PHP's copy-on-write result, because an over-release of +//! the pre-sort array would surface as a use-after-free in the alias rather than as a leak. +//! - Expected stdout values are real `LC_ALL=C php` 8.4 output for the same fixtures. + +use crate::support::compile_and_run_with_heap_debug; + +/// Asserts the program printed `expected` and left a clean heap under heap debug. +fn assert_clean(out: crate::support::ProgramOutput, expected: &str) { + assert_eq!(out.stdout, expected, "stderr: {}", out.stderr); + assert!( + out.stderr.contains("HEAP DEBUG: leak summary: clean"), + "expected clean heap, got: {}", + out.stderr + ); +} + +/// Mutating an instance property leaves no live heap blocks: each separated copy is written +/// back into the property, the property's previous occupant is released exactly once, and the +/// synthetic local that carried the array is released at scope exit. +/// +/// The fixture deliberately avoids `usort()` with a closure comparator: that combination +/// leaks eight blocks on a plain local too, so it would assert a pre-existing defect rather +/// than this lowering's ownership balance. +#[test] +fn test_property_mutators_leave_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#"items, 9); +array_unshift($b->items, 0); +sort($b->items); +array_pop($b->items); +echo implode(",", $b->items); +"#, + ); + assert_clean(out, "0,1,2,3"); +} + +/// The aliased property case: the pre-sort array is still owned by `$copy`, so the +/// write-back's release of the property's previous occupant must not free it. +#[test] +fn test_sort_on_aliased_instance_property_leaves_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#"items; +sort($b->items); +echo implode(",", $b->items), "|", implode(",", $copy); +"#, + ); + assert_clean(out, "1,2,3|3,1,2"); +} + +/// A static-property load is a borrowed pointer, so the synthetic local has to retain it +/// before the sort separates a copy; otherwise the write-back frees the aliased original. +#[test] +fn test_sort_on_aliased_static_property_leaves_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#" [3,1,2]]; +$copy = $m["k"]; +sort($m["k"]); +echo implode(",", $m["k"]), "|", implode(",", $copy); +"#, + ); + assert_clean(out, "1,2,3|3,1,2"); +} + +/// Repeated mutation of one property inside a loop: each iteration reads, separates, and +/// writes back, so an unbalanced release or retain accumulates instead of staying flat. +#[test] +fn test_repeated_property_mutation_in_loop_leaves_clean_heap() { + let out = compile_and_run_with_heap_debug( + r#"items, $i); + sort($b->items); +} +echo implode(",", $b->items); +"#, + ); + assert_clean(out, "0,1,1,2,2,3,3,4"); +} diff --git a/tests/codegen/runtime_gc/foreach_object_source.rs b/tests/codegen/runtime_gc/foreach_object_source.rs new file mode 100644 index 0000000000..08e2292b94 --- /dev/null +++ b/tests/codegen/runtime_gc/foreach_object_source.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Regression tests for the reference a `foreach` loop holds on an *object* +//! source — a `Generator`, or a user class implementing `Iterator`. That +//! reference used to be taken by a bare backend `incref` inside `Op::IterStart` +//! that nothing ever balanced, so every such loop leaked the iterated object +//! and every heap block it owned. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Each fixture runs under `--heap-debug` and asserts `leak summary: clean`, +//! so a reintroduced imbalance fails here instead of silently growing the heap. +//! - The reference is now an `Op::Acquire` released by the loop's exit block and +//! its `LoopCleanup`, which is also what keeps `unset($it)` inside the body +//! from freeing the object mid-iteration — hence the `unset` fixtures, which +//! would regress into a use-after-free if the acquire were simply dropped. +//! - Expected stdout is real `LC_ALL=C php` 8.4 output. + +use crate::support::compile_and_run_with_heap_debug; + +/// Asserts the program printed `expected` and left a clean heap under heap debug. +fn assert_clean(out: crate::support::ProgramOutput, expected: &str) { + assert_eq!(out.stdout, expected, "stderr: {}", out.stderr); + assert!( + out.stderr.contains("HEAP DEBUG: leak summary: clean"), + "expected clean heap, got: {}", + out.stderr + ); +} + +/// The reported repro: a fully consumed two-value generator must not leak. It +/// used to end the program with `live_blocks=5` — the generator object plus the +/// persistent key/value/return cells it owns, none of which were reclaimed +/// because the generator's refcount never reached zero. +#[test] +fn test_foreach_over_generator_temporary_heap_clean() { + let out = compile_and_run_with_heap_debug( + r#"getMessage(); } +echo "\n"; +"#, + ); + assert_clean(out, "1C:x\n"); +} + +/// The same imbalance affected every object source, not only generators: a +/// user class implementing `Iterator` leaked one object per loop. +#[test] +fn test_foreach_over_user_iterator_heap_clean() { + let out = compile_and_run_with_heap_debug( + r#"i; } + public function key(): mixed { return $this->i; } + public function next(): void { $this->i++; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 3; } +} +$c = new Counter(); +foreach ($c as $v) { echo $v; } +foreach (new Counter() as $v) { echo $v; } +echo "\n"; +"#, + ); + assert_clean(out, "012012\n"); +} + +/// PHP keeps the iterated object alive for the whole loop even when the body +/// drops every other owner, so dropping the loop's own reference instead of +/// balancing it would turn this fixture into a use-after-free that stops after +/// one iteration. Covers both `unset()` and a plain rebind. +#[test] +fn test_foreach_body_dropping_source_variable_keeps_iterating() { + let out = compile_and_run_with_heap_debug( + r#"i; } + public function key(): mixed { return $this->i; } + public function next(): void { $this->i++; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 3; } +} +function gen() { yield 1; yield 2; } +$a = new Counter(); +foreach ($a as $v) { echo $v; unset($a); } +$b = new Counter(); +foreach ($b as $v) { echo $v; $b = null; } +$g = gen(); +foreach ($g as $v) { echo $v; unset($g); } +echo "\n"; +"#, + ); + assert_clean(out, "01201212\n"); +} diff --git a/tests/codegen/runtime_gc/regressions.rs b/tests/codegen/runtime_gc/regressions.rs index 76bdbc420c..0b1e0827c3 100644 --- a/tests/codegen/runtime_gc/regressions.rs +++ b/tests/codegen/runtime_gc/regressions.rs @@ -4491,3 +4491,33 @@ echo implode("", $r), "\n"; out.stderr ); } + +/// Ownership regression for the newly added bounded-scratch string producers. +/// +/// `chunk_split()`, `quotemeta()`, and `base_convert()` all write into a reservation taken +/// from `__rt_concat_reserve`, so none of them can alias an argument. Leaving `chunk_split` +/// in the default `MayAliasArguments` bucket suppressed the release of its owned subject +/// temporary and leaked one block per call; `Independent`/`Fresh` ownership keeps the loop +/// below clean. +#[test] +fn test_scratch_string_builtins_release_owned_argument_temporaries() { + let out = compile_and_run_with_heap_debug( + r#"valid()); ) ); } + +// Tests that an SplFixedArray size whose `size * 8` storage payload wraps the machine word is +// rejected by the shared `__rt_array_new` guard instead of allocating a tiny block behind a header +// that advertises 2^61 slots. PHP reports the same class of failure as +// "Possible integer overflow in memory allocation". +/// Verifies that an unrepresentable SplFixedArray size aborts instead of over-reporting capacity. +#[test] +fn test_spl_fixed_array_overflowing_size_is_fatal() { + let err = compile_and_run_expect_failure( + r#"getSize(); +"#, + ); + assert!( + err.contains("requested array size exceeds the maximum allowed array size"), + "{}", + err + ); +} + +// Positive control for the SplFixedArray storage-size guard: an ordinary fixed array still +// allocates, zero-initializes, and round-trips element writes. +/// Verifies that ordinary SplFixedArray allocation is unaffected by the storage-size guard. +#[test] +fn test_spl_fixed_array_normal_size_still_works() { + let out = compile_and_run( + r#"getSize(), ":", $fixed[1], ":", $fixed[0] === null ? "null" : "set"; +"#, + ); + assert_eq!(out, "3:7:null"); +} diff --git a/tests/codegen/stack_guard.rs b/tests/codegen/stack_guard.rs new file mode 100644 index 0000000000..dca12f511f --- /dev/null +++ b/tests/codegen/stack_guard.rs @@ -0,0 +1,159 @@ +//! Purpose: +//! Integration or regression tests for the call-stack overflow guard: unbounded recursion +//! must end in a controlled fatal on stderr with a non-zero exit instead of a raw SIGSEGV, +//! while legitimately deep recursion must keep working on both the OS stack and the +//! coroutine stacks used by generators and fibers. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Inline PHP fixtures are compiled to native binaries; the failure cases assert on the +//! stderr text and on the mere fact that the process did not succeed, because a raw +//! signal death would also be non-zero — the message is what proves the guard fired. +//! - The "still succeeds" cases are the false-positive gate: they must stay comfortably +//! under the real limits (roughly 50k frames on a default 8 MiB OS stack and roughly 1k +//! frames on a 256 KiB fiber stack) so the reserve can never make them flaky. + +use crate::support::*; + +/// PHP 8.3+ reports runaway recursion as `Maximum call stack size ... reached. Infinite +/// recursion?`; elephc reports the same condition with the same wording minus the byte +/// count and source location, which the fatal path cannot produce. +const OVERFLOW_MESSAGE: &str = "Maximum call stack size reached. Infinite recursion?"; + +/// Verifies that direct unbounded recursion reports the controlled call-stack fatal on +/// stderr instead of dying from SIGSEGV with no diagnostic. +#[test] +fn test_direct_infinite_recursion_reports_controlled_fatal() { + let err = compile_and_run_expect_failure( + r#"go($n + 1); return $x; } +} +$r = new Recurse(); +echo $r->go(0); +"#, + ); + assert!(err.contains(OVERFLOW_MESSAGE), "{err}"); +} + +/// False-positive gate: a linked-list style walk 20 000 frames deep and a recursive +/// Fibonacci must both still run to completion. If the guard's reserve were too large, or +/// the published floor were computed from the wrong stack, this is what would break. +#[test] +fn test_deep_but_legitimate_recursion_still_succeeds() { + let out = compile_and_run( + r#"start(); +echo "unreachable"; +"#, + ); + assert!(err.contains(OVERFLOW_MESSAGE), "{err}"); +} + +/// False-positive gate for coroutine stacks: a generator and a fiber that each recurse a +/// few hundred frames deep must still produce their values. This is the case that would +/// break if the fiber floor were left pointing at the OS-thread stack. +#[test] +fn test_bounded_recursion_inside_generator_and_fiber_still_succeeds() { + let out = compile_and_run( + r#"start(); +"#, + ); + assert_eq!(out, "450,300"); +} + +/// Verifies that the OS-thread floor is restored when a fiber suspends back to the main +/// stack: deep main-stack recursion after a fiber round trip must still succeed, which it +/// cannot if `_stack_limit` were left holding the coroutine floor. +#[test] +fn test_main_stack_floor_is_restored_after_a_fiber_round_trip() { + let out = compile_and_run( + r#"start(); +$f->resume(); +echo walk(20000); +"#, + ); + assert_eq!(out, "20000"); +} diff --git a/tests/codegen/strict_php.rs b/tests/codegen/strict_php.rs index e6db34369a..ff46aca12d 100644 --- a/tests/codegen/strict_php.rs +++ b/tests/codegen/strict_php.rs @@ -99,6 +99,32 @@ echo ptr_get(41); assert_eq!(out, "42"); } +/// Verifies every call form dispatches a strict-hidden extension builtin name to the user +/// function that shadows it, so callable strings cannot pick a different target than a +/// direct call. +/// +/// The five forms are a direct call, a variable holding the name, `call_user_func()` with +/// that variable, `call_user_func()` with the literal name, and a first-class callable. +/// `ptr_is_null` is an extension builtin `--strict-php` hides, so only the user function is +/// visible here and all five must answer `7`. Pinned on its own so the rule does not depend +/// on the mixed PHP/LFC include-graph fixture in `tests/codegen/lfc.rs`. +#[test] +fn test_strict_php_user_shadowed_extension_name_dispatches_the_same_from_every_call_form() { + let out = compile_strict_cli_and_run( + r#"getMessage(), "\n"; } +try { chunk_split("ab", -1, "|"); } catch (\ValueError $e) { echo $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "chunk_split(): Argument #2 ($length) must be greater than 0\nchunk_split(): Argument #2 ($length) must be greater than 0" + ); +} + +/// Verifies `chunk_split()` of a result larger than the 64 KiB concat scratch buffer keeps +/// every chunk boundary intact through the bounded heap fallback. +#[test] +fn test_chunk_split_result_larger_than_concat_scratch() { + let out = compile_and_run( + r#" $word) { echo $offset, ":", $word, " "; } +"#, + ); + assert_eq!(out, "0:Hello 6:friend 14:you're 21:here "); +} + +/// Verifies `str_word_count()` honours the extra `$characters` alphabet and php-src's rule +/// that a leading `'`/`-` and a trailing `-` are dropped unless the list re-admits them. +#[test] +fn test_str_word_count_characters_and_boundaries() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { str_word_count("ab", -1); } catch (\ValueError $e) { echo $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "str_word_count(): Argument #2 ($format) must be a valid format value\nstr_word_count(): Argument #2 ($format) must be a valid format value" + ); +} + +/// Verifies `str_word_count()` format 1 keeps growing its result array well past the initial +/// capacity, so the appended words survive every reallocation. +#[test] +fn test_str_word_count_list_grows_past_initial_capacity() { + let out = compile_and_run( + r#" $count) { echo $byte, "=", $count, " "; } +echo "|", count_chars("hello world", 3), "|", strlen(count_chars("hello world", 4)); +"#, + ); + assert_eq!( + out, + "32=1 100=1 101=1 104=1 108=3 111=2 114=1 119=1 | dehlorw|248" + ); +} + +/// Verifies `count_chars()` mode 0 (and the omitted default) tallies all 256 byte values while +/// mode 2 keeps only the ones the subject never uses. +#[test] +fn test_count_chars_full_and_unused_tallies() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { count_chars("ab", -1); } catch (\ValueError $e) { echo $e->getMessage(); } +"#, + ); + assert_eq!( + out, + "count_chars(): Argument #2 ($mode) must be between 0 and 4 (inclusive)\ncount_chars(): Argument #2 ($mode) must be between 0 and 4 (inclusive)" + ); +} + +/// Verifies `strtr()` replacement pairs apply longest-match-first in one left-to-right pass +/// with no re-substitution of already replaced text. +#[test] +fn test_strtr_replacement_pairs() { + let out = compile_and_run( + r#""bar","bar"=>"baz"]), "|"; +echo strtr("hi all, I said hello", ["hello"=>"hi","hi"=>"hello"]), "|"; +echo strtr("abc", ["a"=>"b","ab"=>"X"]), "|"; +echo strtr("abcabc", ["abc"=>"x","bca"=>"y"]), "|"; +echo strtr("aXbXc", ["X"=>"","b"=>"BB"]); +"#, + ); + assert_eq!(out, "bar baz|hello all, I said hi|Xc|xx|aBBc"); +} + +/// Verifies `strtr()` skips keys longer than the subject, matches numeric-string and integer +/// keys through their decimal spelling, and returns the subject for an empty pair list. +#[test] +fn test_strtr_key_edge_cases() { + let out = compile_and_run( + r#""X"]), "|"; +echo strtr("12345", [1=>"one", 23=>"two-three"]), "|"; +echo strtr("0a1", ["0"=>"zero","1"=>"one"]), "|"; +echo strtr("abc", []), "|"; +echo strtr("abc", ["a","b"]); +"#, + ); + assert_eq!(out, "abc|onetwo-three45|zeroaone|abc|abc"); +} + +/// Verifies the three-argument `strtr()` byte translation truncates to the shorter list, never +/// re-translates an already written byte, and lets a later pair win for the same source byte. +#[test] +fn test_strtr_pairwise() { + let out = compile_and_run( + r#""B"]; echo StrTr("abc", "abc", "xyz"), "|", \strtr("abc", ["a"=>"1"]), "|", strtr(string: "abc", from: $map), "|", strtr(string: "abc", from: "ab", to: "xy");"#, + ); + assert_eq!(out, "xyz|1bc|aBc|xyc"); +} + +/// Verifies a `strtr()` result far larger than the 64 KiB concat scratch buffer stays intact +/// through the bounded heap fallback. +#[test] +fn test_strtr_result_larger_than_concat_scratch() { + let out = compile_and_run( + r#" "cdef"]); +echo strlen($out), "|", substr($out, 0, 8), "|", substr($out, -8); +"#, + ); + assert_eq!(out, "200000|cdefcdef|cdefcdef"); +} + +/// Verifies `quoted_printable_encode()` escapes exactly the byte classes php-src escapes. +/// +/// Control bytes, `0x7F`, high-bit bytes, and `=` itself always become `=XX`; ordinary +/// printable ASCII is copied through. A TRAILING space stays a literal space (php-src only +/// escapes a space that is directly followed by a `CR`), while a trailing tab is a control +/// byte and always becomes `=09`. Expected values are verbatim `LC_ALL=C php` 8.4.20 output. +#[test] +fn test_quoted_printable_encode_escapes_php_byte_classes() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } +} +"#, + ); + assert_eq!( + out, + "substr_count(): Argument #2 ($needle) must not be empty\n\ +substr_count(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +substr_count(): Argument #4 ($length) must be contained in argument #1 ($haystack)\n" + ); +} + +/// Verifies `strncmp()` compares only the first `$length` bytes and returns php-src's raw +/// byte difference. `LC_ALL=C php` prints `0`, `-12`, `-1`, `1`, `0` for these calls. +#[test] +fn test_strncmp_prefix_and_byte_difference() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { strncasecmp("a", "b", -1); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +"#, + ); + assert_eq!( + out, + "strncmp(): Argument #3 ($length) must be greater than or equal to 0\n\ +strncasecmp(): Argument #3 ($length) must be greater than or equal to 0\n" + ); +} + +/// Verifies `join()`, `substr_count()`, `strncmp()`, and `strncasecmp()` keep their PHP +/// types inside an array literal, whose element typing uses the checker's syntactic +/// inference table rather than the per-call checked type. +#[test] +fn test_new_string_builtins_keep_their_types_inside_array_literals() { + let out = compile_and_run( + r#"\n string(3) \"a-b\"\n [1]=>\n int(3)\n [2]=>\n int(-1)\n [3]=>\n int(0)\n}\n" + ); +} + +/// Verifies `strpos()` accepts PHP's third `$offset` argument positionally and by name, and +/// resolves a negative offset against the haystack length. +/// Expected values are verbatim `LC_ALL=C php` 8.4 output for the same program. +#[test] +fn test_strpos_offset_positional_and_named() { + let out = compile_and_run( + r#" 100 ? "z" : ""); +$needle = "bc"; +var_dump(strpos($haystack, $needle, $argc + 1)); +var_dump(strrpos($haystack, $needle, -$argc - 2)); +var_dump(strrpos($haystack, $needle, offset: -$argc - 5)); +"#, + ); + assert_eq!(out, "int(4)\nint(1)\nbool(false)\n"); +} + +/// Verifies both position builtins raise php-src's catchable `ValueError` for an `$offset` +/// that does not land inside the haystack, in either direction. +/// Messages are verbatim `LC_ALL=C php` 8.4 output. +#[test] +fn test_string_position_offset_out_of_range_value_errors() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { strpos("abc", "a", -4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +try { strrpos("abc", "a", 4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +try { strrpos("abc", "a", -4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +"#, + ); + assert_eq!( + out, + "strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +strrpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +strrpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n" + ); +} + +/// Verifies `stripos()` finds the FIRST case-insensitive occurrence of a needle. +/// +/// Folding is ASCII-only, matching php-src's locale-independent `zend_tolower_ascii`: the +/// bracket/brace case checks that the byte range just outside `A`-`Z` is compared verbatim, +/// and `stripos("Été", "é")` is 3 rather than 1 because `0x89` and `0xA9` do not fold onto +/// each other. Expected values are verbatim `LC_ALL=C php` 8.4.20 output. +#[test] +fn test_stripos_finds_first_case_insensitive_match() { + let out = compile_and_run( + r#" 100 ? "z" : ""); +$needle = "bC"; +var_dump(stripos($haystack, $needle, $argc + 1)); +var_dump(strripos($haystack, $needle, -$argc - 2)); +var_dump(strripos($haystack, $needle, offset: -$argc - 5)); +"#, + ); + assert_eq!(out, "int(4)\nint(1)\nbool(false)\n"); +} + +/// Verifies both case-insensitive position builtins raise php-src's catchable `ValueError` +/// for an `$offset` that does not land inside the haystack, in either direction. +/// Messages are verbatim `LC_ALL=C php` 8.4.20 output. +#[test] +fn test_case_insensitive_position_offset_out_of_range_value_errors() { + let out = compile_and_run( + r#"getMessage(), "\n"; } +try { stripos("abc", "a", -4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +try { strripos("abc", "a", 4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +try { strripos("abc", "a", -4); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } +"#, + ); + assert_eq!( + out, + "stripos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +stripos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +strripos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n\ +strripos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)\n" + ); +} + +/// Verifies `stripos()`/`strripos()` through case-insensitive, namespaced, and dynamic call +/// sites, so the registry catalog resolves all three spellings to the same runtime target. +#[test] +fn test_case_insensitive_position_case_insensitive_and_namespaced() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|", str_repeat("ab", 0), "|", str_repeat("ab", 2); +"#, + ); + assert_eq!( + out, + "ValueError|str_repeat(): Argument #2 ($times) must be greater than or equal to 0||abab" + ); +} + /// Verifies strrev reverses the characters in a string. #[test] fn test_strrev() { @@ -249,6 +272,181 @@ echo count($parts) . " " . $parts[0] . " " . $parts[1] . " " . $parts[2]; assert_eq!(out, "3 He ll o"); } +/// Verifies `str_pad()` with an empty `$pad_string` raises PHP's catchable `ValueError`. +/// +/// The runtime pad loop copied `$length - strlen($string)` bytes out of the pad string, so an +/// empty pad string made it read whatever followed the zero-length buffer: `str_pad("x", 4, "")` +/// returned `"xUUU"` built from uninitialized memory. PHP only rejects the empty pad string when +/// padding would actually happen, so the shorter-`$length` call must still return the input. +#[test] +fn test_str_pad_empty_pad_string_is_a_catchable_value_error() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|", str_pad("xyz", 1, ""), "|", str_pad("x", 4, "-", STR_PAD_LEFT); +"#, + ); + assert_eq!( + out, + "ValueError|str_pad(): Argument #3 ($pad_string) must not be empty|xyz|---x" + ); +} + +/// Verifies an uncaught empty `str_pad()` pad string reports PHP's uncaught-`ValueError` fatal. +#[test] +fn test_str_pad_empty_pad_string_uncaught_reports_value_error_fatal() { + let err = compile_and_run_expect_failure(r#"getMessage(); +} +echo "|", str_pad("x", 5, "ab", STR_PAD_BOTH); +"#, + ); + assert_eq!( + out, + "ValueError|str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH|abxab" + ); +} + +/// Verifies `str_split()` with a non-positive `$length` raises PHP's catchable `ValueError`. +/// +/// `__rt_str_split` advanced its cursor by the chunk length, so `0` spun forever pushing empty +/// chunks until the heap was exhausted and `-1` walked the cursor backwards and crashed. +#[test] +fn test_str_split_non_positive_length_is_a_catchable_value_error() { + let out = compile_and_run( + r#"getMessage(), "\n"; + } +} +echo implode(",", str_split("abcde", 2)), "\n"; +"#, + ); + assert_eq!( + out, + "ValueError|str_split(): Argument #2 ($length) must be greater than 0\n\ + ValueError|str_split(): Argument #2 ($length) must be greater than 0\n\ + ab,cd,e\n" + ); +} + +/// Verifies an uncaught zero `str_split()` chunk length reports PHP's uncaught-`ValueError` fatal. +#[test] +fn test_str_split_zero_length_uncaught_reports_value_error_fatal() { + let err = compile_and_run_expect_failure(r#"getMessage(); +} +echo "|", implode("/", explode(",", "a,b,c")); +"#, + ); + assert_eq!( + out, + "ValueError|explode(): Argument #1 ($separator) must not be empty|a/b/c" + ); +} + +/// Verifies an uncaught empty `explode()` separator reports PHP's uncaught-`ValueError` fatal. +#[test] +fn test_explode_empty_separator_uncaught_reports_value_error_fatal() { + let err = compile_and_run_expect_failure(r#"getMessage(); +} +echo "|"; +try { + echo wordwrap("abcdef", 0, "\n", true); +} catch (ValueError $e) { + echo get_class($e), "|", $e->getMessage(); +} +echo "|", wordwrap("ab cd", 3, "|"), "|", wordwrap("abcdef", 0, "\n", false); +"#, + ); + assert_eq!( + out, + "ValueError|wordwrap(): Argument #3 ($break) must not be empty|\ + ValueError|wordwrap(): Argument #4 ($cut_long_words) cannot be true when argument #2 ($width) is 0|\ + ab|cd|abcdef" + ); +} + /// Verifies sprintf zero-pads an integer to a given width. #[test] fn test_sprintf_zero_padded_int() { @@ -292,3 +490,267 @@ fn test_multiarg_string_builtins_of_mixed_argument() { ); assert_eq!(out, "hell0 w0rld|6|hello,world"); } + +/// Verifies `str_pad()` to a target width far beyond the 64 KiB concat scratch buffer produces the +/// full padded string instead of running the pad loop past the scratch end (overflow regression). +#[test] +fn test_str_pad_target_larger_than_concat_scratch() { + let out = compile_and_run( + r#"&'", 8000)); +echo strlen($h), "|", substr($h, 0, 12), "|", substr($h, -12); +"#, + ); + assert_eq!(out, "312000|<a href=&|;&'"); +} + +/// Verifies `html_entity_decode()` of an entity-encoded payload longer than the 64 KiB concat +/// scratch buffer decodes every entity through the heap fallback. +#[test] +fn test_html_entity_decode_input_larger_than_concat_scratch() { + let out = compile_and_run( + r#"&\"'&\"'"); +} + +/// Verifies `nl2br()` on an all-newline-heavy payload whose worst-case 7x expansion exceeds the +/// 64 KiB concat scratch buffer keeps every injected break tag intact. +#[test] +fn test_nl2br_result_larger_than_concat_scratch() { + let out = compile_and_run( + r#"\n|b
\n"); +} + +/// Verifies `addslashes()` / `stripslashes()` round-trip a payload whose 2x escaped form exceeds +/// the 64 KiB concat scratch buffer, so both directions take the heap fallback and stay byte-exact. +#[test] +fn test_addslashes_roundtrip_larger_than_concat_scratch() { + let out = compile_and_run( + r#"", true); +echo strlen($w), "|", substr($w, 0, 14), "|", substr($w, -9); +"#, + ); + assert_eq!(out, "144000|hello
world|world
"); +} + +/// Verifies `str_replace()` with an expanding replacement whose result exceeds the 64 KiB concat +/// scratch buffer emits every replacement instead of overrunning the scratch end. +#[test] +fn test_str_replace_expansion_larger_than_concat_scratch() { + let out = compile_and_run( + r#" 100 ? "z" : ""); +$separators = "|" . ($argc > 100 ? "" : "-"); +var_dump(ucwords($subject, $separators)); +var_dump(ucwords($subject, separators: $separators)); +var_dump(ucwords($subject)); +"#, + ); + assert_eq!( + out, + "string(17) \"Hello|World-Again\"\n\ +string(17) \"Hello|World-Again\"\n\ +string(17) \"Hello|world-again\"\n" + ); +} diff --git a/tests/codegen/support/compiler.rs b/tests/codegen/support/compiler.rs index cf68070119..20d8b06bf9 100644 --- a/tests/codegen/support/compiler.rs +++ b/tests/codegen/support/compiler.rs @@ -155,6 +155,72 @@ fn compile_source_to_asm_with_defines_repr_regex_and_php_version( with_regex: bool, php_version: elephc::php_version::PhpVersion, ) -> (String, String, TestLinkRequirements) { + let (user_asm, runtime_asm, link_requirements) = try_compile_source_to_asm_with_defines_repr( + source, + dir, + defines, + heap_size, + gc_stats, + heap_debug, + null_repr, + with_regex, + php_version, + ); + ( + user_asm.expect("EIR backend codegen failed for codegen fixture"), + runtime_asm, + link_requirements, + ) +} + +/// Compiles a snippet and returns the EIR backend's diagnostic text, asserting that the +/// backend refused the program. +/// +/// Backend refusals (`unsupported EIR backend feature: …`) are raised after type checking, +/// so `tests/error_tests.rs` — which stops at the checker — cannot observe them. Use this +/// for shapes elephc deliberately declines to compile. +pub(crate) fn compile_source_expect_backend_error(source: &str) -> String { + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let tid = std::thread::current().id(); + let pid = std::process::id(); + let dir = std::env::temp_dir().join(format!("elephc_test_{}_{:?}_{}", pid, tid, id)); + fs::create_dir_all(&dir).unwrap(); + let (user_asm, _runtime_asm, _link_requirements) = try_compile_source_to_asm_with_defines_repr( + source, + &dir, + &HashSet::new(), + 8_388_608, + false, + false, + default_null_repr(), + false, + elephc::php_version::PhpVersion::default(), + ); + let _ = fs::remove_dir_all(&dir); + match user_asm { + Ok(_) => panic!("expected the EIR backend to reject this program, but it compiled"), + Err(error) => error.to_string(), + } +} + +/// Runs the codegen-fixture pipeline and hands back the backend's `Result` instead of +/// unwrapping it, so callers can assert on either outcome. +#[allow(clippy::too_many_arguments)] +fn try_compile_source_to_asm_with_defines_repr( + source: &str, + dir: &Path, + defines: &HashSet, + heap_size: usize, + gc_stats: bool, + heap_debug: bool, + null_repr: elephc::codegen::NullRepr, + with_regex: bool, + php_version: elephc::php_version::PhpVersion, +) -> ( + std::result::Result, + String, + TestLinkRequirements, +) { elephc::codegen::set_null_repr(null_repr); let tokens = elephc::lexer::tokenize(source).expect("tokenize failed"); let ast = elephc::parser::parse(&tokens).expect("parse failed"); @@ -175,6 +241,10 @@ fn compile_source_to_asm_with_defines_repr_regex_and_php_version( let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = elephc::autoload::run(resolved, dir, &autoload_registry).expect("autoload failed"); + // Mirrors `pipeline::compile`: `func_num_args`/`func_get_args`/`func_get_arg` are + // desugared into a hidden variadic parameter plus plain PHP after autoloading and + // before the optimizer, so the checker and the backend only ever see ordinary PHP. + let resolved = elephc::func_args::desugar(resolved).expect("func_args desugar failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -204,8 +274,7 @@ fn compile_source_to_asm_with_defines_repr_regex_and_php_version( &exported_functions, regalloc_linear, false, - ) - .expect("EIR backend codegen failed for codegen fixture"); + ); let runtime_features = ir_module.required_runtime_features; let runtime_asm = elephc::codegen::generate_runtime_with_features(heap_size, target(), runtime_features); diff --git a/tests/codegen/support/projects.rs b/tests/codegen/support/projects.rs index 53a0d9ab3a..520724f80c 100644 --- a/tests/codegen/support/projects.rs +++ b/tests/codegen/support/projects.rs @@ -229,6 +229,9 @@ pub(crate) fn compile_expect_type_error(source: &str) -> String { let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = elephc::autoload::run(resolved, &dir, &autoload_registry).expect("autoload failed"); + // Mirrors `pipeline::compile`: desugar `func_num_args`/`func_get_args`/`func_get_arg` + // into a hidden variadic parameter plus plain PHP before the optimizer and the checker. + let resolved = elephc::func_args::desugar(resolved).expect("func_args desugar failed"); let resolved = elephc::optimize::fold_constants(resolved); let error = match elephc::types::check_with_target(&resolved, target()) { Ok(_) => panic!("source unexpectedly passed type checking"), @@ -284,6 +287,9 @@ pub(crate) fn compile_and_run_files_expect_failure( let resolved = elephc::resolver::resolve(ast, base_dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + // Mirrors `pipeline::compile`: desugar `func_num_args`/`func_get_args`/`func_get_arg` + // into a hidden variadic parameter plus plain PHP before the optimizer and the checker. + let resolved = elephc::func_args::desugar(resolved).expect("func_args desugar failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -358,6 +364,9 @@ pub(crate) fn compile_and_run_files_with_defines( let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = elephc::autoload::run(resolved, base_dir, &autoload_registry).expect("autoload failed"); + // Mirrors `pipeline::compile`: desugar `func_num_args`/`func_get_args`/`func_get_arg` + // into a hidden variadic parameter plus plain PHP before the optimizer and the checker. + let resolved = elephc::func_args::desugar(resolved).expect("func_args desugar failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); @@ -439,6 +448,7 @@ pub(crate) fn compile_files_fails_with_defines( let resolved = elephc::resolver::resolve(ast, base_dir)?; let resolved = elephc::autoload::collect_aliases(resolved); let resolved = elephc::name_resolver::resolve(resolved)?; + let resolved = elephc::func_args::desugar(resolved)?; let resolved = elephc::optimize::fold_constants(resolved); elephc::types::check_with_target(&resolved, target())?; Ok(()) @@ -466,6 +476,9 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + // Mirrors `pipeline::compile`: desugar `func_num_args`/`func_get_args`/`func_get_arg` + // into a hidden variadic parameter plus plain PHP before the optimizer and the checker. + let resolved = elephc::func_args::desugar(resolved).expect("func_args desugar failed"); let resolved = elephc::optimize::fold_constants(resolved); let check_result = elephc::types::check_with_target(&resolved, target()).expect("type check failed"); diff --git a/tests/codegen/type_builtins/division.rs b/tests/codegen/type_builtins/division.rs index da0b60cf80..9deda95116 100644 --- a/tests/codegen/type_builtins/division.rs +++ b/tests/codegen/type_builtins/division.rs @@ -58,11 +58,27 @@ fn test_intdiv_negative() { assert_eq!(out, "-3"); } -/// Verifies float division by zero produces `INF`. +/// Verifies float division by zero raises PHP 8's `DivisionByZeroError`. +/// +/// This test previously asserted `INF`, which is pre-PHP-8 behaviour: PHP 5/7 warned and +/// yielded `INF`, but PHP 8 made **every** `/` by zero throw, floats included. Verified +/// against reference PHP 8.4: +/// +/// ```text +/// $ php -r 'echo 1.0 / 0.0;' +/// PHP Fatal error: Uncaught DivisionByZeroError: Division by zero +/// ``` +/// +/// `fdiv(1.0, 0.0)` is the function that still returns `INF`, and it is covered separately — +/// it is the reason the old expectation looked plausible. #[test] -fn test_division_by_zero_inf() { - let out = compile_and_run(" 30, "a" => 10]); +echo "|"; +show(...["a" => 10], ...[20]); +echo "|"; +show(...["b" => 20, "a" => 10], c: 30); +"#, + ); + assert_eq!(out, "10/2/30|10/20/3|10/2/30|10/20/3|10/20/30"); +} + +/// Pins that a string-keyed unpack into a *runtime* string callable — the +/// surface that has no signature to plan against — keeps working. The +/// unpack-after-named guard added for that surface must only reject a `...` +/// that follows a literal named argument. +#[test] +fn test_string_callable_named_spread_stays_legal() { + let out = compile_and_run( + r#" 2, "a" => 1]); +"#, + ); + assert_eq!(out, "1|2"); +} diff --git a/tests/codegen/types/narrowing.rs b/tests/codegen/types/narrowing.rs index b6c741bb5c..ed76186f34 100644 --- a/tests/codegen/types/narrowing.rs +++ b/tests/codegen/types/narrowing.rs @@ -591,3 +591,136 @@ fn test_narrow_after_never_call_before_unreachable_code() { ); assert_eq!(out, "a=b"); } + +/// Verifies the classic singleton: a nullable static property narrowed by an `=== null` guard +/// whose then-branch assigns it is non-null on both merge paths, so the `: S` return checks. +#[test] +fn test_nullable_static_property_singleton_narrows_after_if_assign() { + let out = compile_and_run( + r#"v, S::get()->v; +"#, + ); + assert_eq!(out, "77"); +} + +/// Verifies `!isset(self::$p)` narrows the same way `self::$p === null` does. +#[test] +fn test_nullable_static_property_singleton_narrows_after_isset_guard() { + let out = compile_and_run( + r#"v; +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies `self::$p ??= new S();` leaves the static property non-null for the following return. +#[test] +fn test_nullable_static_property_narrows_after_null_coalescing_assign() { + let out = compile_and_run( + r#"v; +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies the early-return singleton shape: `!== null` narrows the guarded return, and the +/// assignment after the `if` narrows the fall-through return. +#[test] +fn test_nullable_static_property_narrows_after_strict_not_null_early_return() { + let out = compile_and_run( + r#"v, S::get()->v; +"#, + ); + assert_eq!(out, "77"); +} + +/// Verifies the same narrowing on an INSTANCE property, through both the guarded-assign and the +/// `??=` shapes. +/// +/// The two shapes live on separate classes on purpose: `$obj->prop ??= ` currently +/// miscompiles when the property is ALREADY set (an unrelated, pre-existing EIR gap that also +/// reproduces without any narrowing), so each `??=` here runs exactly once on a null property. +#[test] +fn test_nullable_instance_property_lazy_initialization_narrows() { + let out = compile_and_run( + r#"n === null) { $this->n = new Node(); } + return $this->n; + } +} +class Lazy { + private ?Node $n = null; + public function get(): Node { + $this->n ??= new Node(); + return $this->n; + } +} +$h = new Holder(); +echo $h->get()->v, $h->get()->v; +$l = new Lazy(); +echo $l->get()->v; +"#, + ); + assert_eq!(out, "333"); +} + +/// Verifies the branch join also fires when both arms assign: the merged fact is the union of the +/// two branch-exit types, not the declared nullable type. +#[test] +fn test_nullable_static_property_join_across_both_branches() { + let out = compile_and_run( + r#"v; +"#, + ); + assert_eq!(out, "7"); +} diff --git a/tests/codegen/types/param_coercion.rs b/tests/codegen/types/param_coercion.rs new file mode 100644 index 0000000000..2b32649513 --- /dev/null +++ b/tests/codegen/types/param_coercion.rs @@ -0,0 +1,117 @@ +//! Purpose: +//! End-to-end coverage for PHP's coercive parameter binding on declared user-defined +//! parameters: scalars widening into `string`/`bool` parameters, and compile-time-constant +//! numeric arguments binding to `int`/`float` parameters. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Every expected value is verbatim `LC_ALL=C php` 8.4.20 stdout. +//! - `$argc` keeps an argument runtime-valued so the binding is exercised on a real value +//! rather than being decided by AST constant folding. +//! - The bindings elephc deliberately refuses (lossy or non-numeric conversions, which PHP +//! signals with a runtime `Deprecated:` notice or `TypeError`) are pinned in +//! `tests/error_tests/type_system.rs`. + +use crate::support::*; + +/// Verifies the parameter-typing audit repro: a float and a numeric string binding to `int`, +/// and an int binding to `string`. +#[test] +fn test_coercive_binding_audit_repro() { + let out = compile_and_run( + r#" 0; + echo fmt($n), fmt($f), fmt($b); + "#, + ); + assert_eq!(out, "[41][5.5][1]"); +} + +/// Verifies numeric-string constants bind to `int` and `float` parameters, covering PHP's +/// surrounding-whitespace allowance, exponent spelling, and a negative value. +#[test] +fn test_numeric_string_constants_bind_to_numeric_parameters() { + let out = compile_and_run( + r#"label . ":" . $s; } + public static function of(int $n): string { return "n=" . $n; } + } + echo (new Box(1.5))->tag(42), " ", Box::of(7.0); + "#, + ); + assert_eq!(out, "1.5:42 n=7"); +} + +/// Verifies the binding fires for named arguments, which reach EIR through the reordered +/// named-argument path rather than the positional one. +#[test] +fn test_coercive_binding_applies_to_named_arguments() { + let out = compile_and_run( + r#" Result<(), String> // arity diagnostics. Injection is gated on usage, so no other test is affected. let ast = elephc::hash_prelude::inject_if_used(ast, false); let ast = elephc::name_resolver::resolve(ast).map_err(|e| e.message.clone())?; + // Mirrors `pipeline::compile`: `func_num_args`/`func_get_args`/`func_get_arg` are + // desugared into a hidden variadic parameter plus plain PHP before the checker runs, so + // their own diagnostics reach this harness instead of a bare `Undefined function`. + let ast = elephc::func_args::desugar(ast).map_err(|e| e.message.clone())?; let ast = elephc::optimize::fold_constants(ast); types::check(&ast).map_err(|e| e.message.clone())?; Ok(()) @@ -51,6 +55,7 @@ fn check_source_full(src: &str) -> Result 0; array_reverse([1, 2], $t);", + "array_reverse() preserve_keys argument must be a literal bool in AOT mode", + ); +} + +/// Verifies `array_chunk()` rejects a non-literal `preserve_keys` flag in AOT mode. +/// +/// The flag decides whether each chunk is a renumbered indexed array or an integer-keyed hash, +/// so it cannot be resolved at run time. +#[test] +fn test_error_array_chunk_non_literal_preserve_keys() { + expect_error( + " 0; array_chunk([1, 2], 1, $t);", + "array_chunk() preserve_keys argument must be a literal bool in AOT mode", + ); +} + +/// Verifies `array_chunk()` reports PHP's full 2-to-3 argument range. +#[test] +fn test_error_array_chunk_wrong_args() { + expect_error( + " 0; array_slice([1, 2], 0, 1, $t);", + "array_slice() preserve_keys argument must be a literal bool in AOT mode", + ); +} + +/// Verifies a key-preserving `array_slice()` of a boxed array is rejected, not miscompiled. +/// +/// The key-preserving helper copies the source header's `value_type` into the result hash, so +/// the element layout has to be known statically. +#[test] +fn test_error_array_slice_preserve_keys_boxed_source() { + expect_error( + r#" strlen($a) <=> strlen($b)); +"#, + ); + expect_no_error( + r#" "banana", "j" => "apple"]; +uasort($w, fn($a, $b) => strlen($a) <=> strlen($b)); +"#, + ); + expect_no_error( + r#" strlen($v) > 3); +"#, + ); + expect_no_error( + r#" strtoupper($v), $w); +"#, + ); + expect_no_error( + r#" $c + strlen($v), 0); +"#, + ); +} + +/// Verifies that `uksort()` types its comparator parameters from the array's KEY type, so a +/// string-keyed array gives an untyped comparator two `string` parameters instead of `int`. +#[test] +fn test_uksort_untyped_parameters_inherit_key_type() { + expect_no_error( + r#" 1, "fig" => 2]; +uksort($w, fn($a, $b) => strlen($a) <=> strlen($b)); +"#, + ); +} + +/// Verifies that `array_walk()` also types the optional second callback parameter from the +/// array's key type, while a single-parameter callback still passes arity validation. +#[test] +fn test_array_walk_callback_second_parameter_inherits_key_type() { + expect_no_error( + r#" 1, "fig" => 2]; +array_walk($w, function ($v, $k) { echo strlen($k), $v; }); +"#, + ); + expect_no_error( + r#" strlen($a) <=> strlen($b)); +"#, + "strlen() argument must be string", + ); +} + +/// Verifies `array_count_values()` rejects a missing argument. +#[test] +fn test_error_array_count_values_wrong_args() { + expect_error( + "p);"#, + "key() argument must be an array variable", + ); +} + +/// Verifies an array-element receiver is a named compile error for the same reason. +/// Fixture: `next($a[0])` on a nested indexed array. +#[test] +fn test_error_array_pointer_element_receiver() { + expect_error( + r#" 0 ? \"strtoupper\" : \"strtolower\"; echo apply($n, \"a\");", + "a callable string must be a compile-time constant here", + ); +} diff --git a/tests/error_tests/io_builtins/filesystem.rs b/tests/error_tests/io_builtins/filesystem.rs index 7c4122341f..bcc2ea113d 100644 --- a/tests/error_tests/io_builtins/filesystem.rs +++ b/tests/error_tests/io_builtins/filesystem.rs @@ -9,12 +9,17 @@ use super::*; -/// Verifies `file_get_contents()` rejects zero arguments with arity error. +/// Verifies `file_get_contents()` rejects both ends of its PHP 8.4 arity range: `$filename` is +/// required and `$length` is the last accepted argument. #[test] fn test_error_file_get_contents_wrong_args() { expect_error( " 30]);", + "Function 'greet' cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches instance method calls. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_method() { + expect_error( + "m(a: 1, ...[\"b\" => 2]);", + "Method C::m cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches static method calls. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_static_method() { + expect_error( + " 2]);", + "Static method C::m cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches constructor calls. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_constructor() { + expect_error( + " 2]);", + "Constructor 'C::__construct' cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches builtin calls. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_builtin() { + expect_error( + " 3]);", + "Builtin 'str_pad' cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches closure calls. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_closure() { + expect_error( + " 2]);", + "callable $f cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule reaches first-class callables. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_first_class_callable() { + expect_error( + " 2]);", + "first-class callable cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule still applies when the callee is a +/// string callable resolved at run time, where no signature is available to plan +/// against. PHP rejects the shape syntactically, so the callee being unknown is +/// not an excuse to accept it. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_string_callable() { + expect_error( + " 2]);", + "callable $c cannot use argument unpacking after named arguments", + ); +} + +/// Verifies that the unpack-after-named rule still applies to `new $class(...)`, +/// where the constructor is not resolvable at compile time. +#[test] +fn test_error_named_arguments_reject_spread_after_named_on_dynamic_constructor() { + expect_error( + " 2]);", + "Dynamic constructor cannot use argument unpacking after named arguments", + ); +} + /// Verifies that even when a spread provides positional arguments, named arguments /// are still processed, and a missing required parameter is still reported. #[test] diff --git a/tests/error_tests/misc/string_and_type_builtins.rs b/tests/error_tests/misc/string_and_type_builtins.rs index da88bba370..7b399a26b8 100644 --- a/tests/error_tests/misc/string_and_type_builtins.rs +++ b/tests/error_tests/misc/string_and_type_builtins.rs @@ -16,11 +16,12 @@ expect_builtin_arity_error!( "strlen() takes exactly 1 argument" ); -// Tests intval() arity error when called with no arguments. +// Tests intval() arity error when called with no arguments. The optional second parameter +// (`$base`) is now accepted, so the derived phrasing is a range rather than an exact count. expect_builtin_arity_error!( test_error_intval_wrong_args, " \"b\"], \"x\");", + "strtr(): Argument #2 ($from) must be of type string, array given" +); + +// Tests str_word_count() rejecting a $format that elephc cannot resolve at compile time. +expect_builtin_arity_error!( + test_error_str_word_count_non_literal_format, + "` with string operands rejects them -/// with the "Spaceship operator requires numeric operands" error. +/// Tests that the spaceship operator `<=>` with *runtime* string operands is rejected with +/// the "Spaceship operator requires numeric operands" error. +/// +/// The operands are locals rather than literals on purpose. Constant folding runs before type +/// checking, and it evaluates a literal `"a" <=> "b"` to PHP's answer (`-1`), so the folded +/// form never reaches the checker and compiles. That is deliberate — PHP defines string +/// comparison, so folding it is PHP-correct — but it leaves the checker gate in +/// `types::checker::inference::ops` as the only thing rejecting the non-constant form. +/// Lifting that gate (and giving the runtime a string comparison path) is issue #507, which +/// covers `<`, `<=`, `>`, `>=` and this operator alike. Until then this test pins the live +/// contract: constant-foldable string comparisons compile, everything else is refused. #[test] fn test_error_spaceship_string() { expect_error( - r#" "b";"#, + r#" $y;"#, "Spaceship operator requires numeric operands", ); } diff --git a/tests/error_tests/misc/system_builtins.rs b/tests/error_tests/misc/system_builtins.rs index 313589052c..655c32efb2 100644 --- a/tests/error_tests/misc/system_builtins.rs +++ b/tests/error_tests/misc/system_builtins.rs @@ -539,3 +539,45 @@ fn test_error_unserialize_non_string_data() { "unserialize() data argument must be string-compatible", ); } + +/// Verifies `constant()` rejects an unknown constant at compile time. +/// +/// Reference PHP raises `Error: Undefined constant "NOPE"` at runtime; an AOT binary has no +/// constant table to look the name up in, so the diagnostic moves to compile time. +#[test] +fn test_error_constant_undefined_name() { + expect_error("p; } + if ($this->p === null) { throw new Exception("x"); } + return $this->p; + } +} +"#, + "return type expects Object(\"A\")", + ); +} + +/// Verifies a nullable static property with no narrowing at all still fails the non-null return. +#[test] +fn test_unguarded_nullable_static_property_return_still_rejected() { + expect_error( + r#"` (PHP alias for `!=`) errors --- + +/// Verifies that `<>` with a missing right operand is reported as an unexpected token +/// rather than silently parsing as `<` followed by `>`. +#[test] +fn test_error_angle_not_equal_missing_right_operand() { + expect_error(" ;", "Unexpected token: Semicolon"); +} + +/// Verifies prefix `++` on `$this` itself (not a member of it) is rejected as an invalid +/// increment target instead of being parsed as a member increment. +#[test] +fn test_error_prefix_increment_on_this_itself() { + expect_error( + "foo()++; } }", + "Invalid assignment target", + ); +} + +// --- foreach destructuring errors --- + +/// Verifies an empty `foreach` destructuring pattern reports PHP's "Cannot use empty list". +#[test] +fn test_error_foreach_empty_destructuring_pattern() { + expect_error(" &$a];", + "Reference elements in array literals (`[&$x]`) are not supported", + ); + expect_error( + "m(true);", + "Method C::m parameter $i expects Int, got Bool", + ); +} + +/// Verifies the directive reaches a closure invoked through a variable, which is validated on a +/// different checker path from a named function call. +#[test] +fn test_error_strict_types_rejects_closure_argument() { + expect_error( + " 'b', 1 => 'a', 3 => 'c']; krsort($a); foreach ($a as $k => $v) { echo $k; echo $v; }", + "3c2b1a", ), ( "natsort_indexed_ints", diff --git a/tests/lexer_tests/keywords/language_keywords.rs b/tests/lexer_tests/keywords/language_keywords.rs index f1665cefe7..30ef0e16d7 100644 --- a/tests/lexer_tests/keywords/language_keywords.rs +++ b/tests/lexer_tests/keywords/language_keywords.rs @@ -419,6 +419,64 @@ fn test_enddeclare_keyword() { assert!(t.contains(&Token::EndDeclare)); } +// --- Alternative control-structure syntax terminators --- + +/// Verifies `if (…): … endif;` lexes the colon body opener and the `endif` terminator. +#[test] +fn test_alternative_if_tokens() { + let t = tokens("` tokenizes as its own `LessGreater` token instead of `Less` followed +/// by `Greater`, while `< >` with a space still yields the two separate tokens. +#[test] +fn test_angle_not_equal_operator() { + let t = tokens(" < > <=>"); + assert_eq!( + t[1..5], + [ + Token::LessGreater, + Token::Less, + Token::Greater, + Token::Spaceship, + ] + ); +} + /// Verifies `&&`, `||`, `and`, `or`, `xor` tokenize as logical operators. #[test] fn test_logical_operators() { diff --git a/tests/parser_tests/control.rs b/tests/parser_tests/control.rs index 24e9fc22e8..0cd9c5e58c 100644 --- a/tests/parser_tests/control.rs +++ b/tests/parser_tests/control.rs @@ -217,3 +217,185 @@ fn test_parse_foreach_key_value_by_ref() { panic!("expected Foreach"); } } + +/// Verifies `foreach ($m as [$a, $b])` desugars to a loop over a hidden value variable whose +/// body starts with the same unpack statement `[$a, $b] = $tmp;` produces. +#[test] +fn test_parse_foreach_value_destructuring_desugars_to_hidden_temp() { + let stmts = parse_source(" [pattern]` form keeps the real key variable and only replaces the +/// value target with the hidden temporary. +#[test] +fn test_parse_foreach_key_with_value_destructuring() { + let stmts = parse_source(" [$a, $b]) {}"); + assert_eq!(stmts.len(), 1); + let StmtKind::Foreach { + key_var, + value_var, + body, + .. + } = &stmts[0].kind + else { + panic!("expected Foreach"); + }; + assert_eq!(key_var, &Some("k".to_string())); + assert!(value_var.starts_with("__elephc_foreach_")); + assert_eq!(body.len(), 1); + assert!(matches!(body[0].kind, StmtKind::ListUnpack { .. })); +} + +/// Verifies a reference to a whole destructuring pattern is rejected: PHP allows `&` on the +/// targets inside the pattern, never on the pattern itself. +#[test] +fn test_parse_foreach_reference_to_pattern_is_rejected() { + assert!(parse_fails(" &[$a, $b]) {}")); +} + +// --- Alternative control-structure syntax --- + +/// Verifies `if (…): … endif;` produces exactly the same `StmtKind::If` shape as the brace form. +#[test] +fn test_alternative_if_parses_to_plain_if() { + let alternative = parse_source("` parses to the same `BinOp::NotEq` node as `!=`, so the alias is +/// indistinguishable from `!=` after parsing. +#[test] +fn test_angle_not_equal_is_alias_of_not_equal() { + let angle = parse_source(" 2;"); + let bang = parse_source("` matches `!=` exactly: it binds looser than +/// `+` and looser than `<`, and it is left-associative like the other equality operators. +#[test] +fn test_angle_not_equal_binding_power_matches_not_equal() { + // Arithmetic (bp 29) binds tighter than `<>` (bp 21): 1 + 2 <> 3 is (1 + 2) <> 3. + let stmts = parse_source(" 3;"); + assert_eq!( + stmts, + vec![Stmt::echo(Expr::binop( + Expr::binop(Expr::int_lit(1), BinOp::Add, Expr::int_lit(2)), + BinOp::NotEq, + Expr::int_lit(3), + ))] + ); + + // Relational (bp 23) binds tighter than `<>` (bp 21): 1 <> 2 < 3 is 1 <> (2 < 3). + let stmts = parse_source(" 2 < 3;"); + assert_eq!( + stmts, + vec![Stmt::echo(Expr::binop( + Expr::int_lit(1), + BinOp::NotEq, + Expr::binop(Expr::int_lit(2), BinOp::Lt, Expr::int_lit(3)), + ))] + ); + + // `<>` is left-associative and shares its level with `==`: 1 <> 2 == 3 is (1 <> 2) == 3. + let stmts = parse_source(" 2 == 3;"); + assert_eq!( + stmts, + vec![Stmt::echo(Expr::binop( + Expr::binop(Expr::int_lit(1), BinOp::NotEq, Expr::int_lit(2)), + BinOp::Eq, + Expr::int_lit(3), + ))] + ); + + // `&&` (bp 13) binds looser than `<>`: 1 <> 2 && 3 is (1 <> 2) && 3. + let stmts = parse_source(" 2 && 3;"); + assert_eq!( + stmts, + vec![Stmt::echo(Expr::binop( + Expr::binop(Expr::int_lit(1), BinOp::NotEq, Expr::int_lit(2)), + BinOp::And, + Expr::int_lit(3), + ))] + ); +} + /// Verifies that `n++;` parses to the same read-modify-write statement as `$this->n += 1;`. +/// Regression: the `$this` statement parser used to reject the trailing `++`. +#[test] +fn test_this_property_postfix_increment_parses_as_compound_assignment() { + assert_eq!( + parse_source("n++;"), + parse_source("n += 1;") + ); + assert_eq!( + parse_source("n--;"), + parse_source("n -= 1;") + ); +} + +/// Verifies prefix `++`/`--` on complex targets parses to the same statement as the +/// equivalent compound assignment, since statement position discards the result. +#[test] +fn test_prefix_increment_on_complex_targets_parses_as_compound_assignment() { + assert_eq!( + parse_source("n;"), + parse_source("n += 1;") + ); + assert_eq!( + parse_source("n;"), + parse_source("n += 1;") + ); + assert_eq!( + parse_source("arr[0]++;` parses to the same statement as the compound assignment, +/// so the array element under a `$this` property is reached as well. +#[test] +fn test_this_property_element_increment_parses_as_compound_assignment() { + assert_eq!( + parse_source("arr[0]++;"), + parse_source("arr[0] += 1;") + ); +} diff --git a/tests/print_r_object_tests.rs b/tests/print_r_object_tests.rs new file mode 100644 index 0000000000..a3e34022cb --- /dev/null +++ b/tests/print_r_object_tests.rs @@ -0,0 +1,421 @@ +//! Purpose: +//! End-to-end tests for `print_r()` of OBJECTS: the `C Object\n(\n [p] => v\n)\n` +//! layout, PHP's visibility-annotated keys, objects nested in arrays and in other +//! objects, enum cases, `print_r($o, true)` return mode, and the `*RECURSION*` +//! guard. +//! +//! Called from: +//! - `cargo test --test print_r_object_tests` through Rust's test harness. +//! +//! Key details: +//! - REGRESSION ANCHOR (issue C5): `print_r()` of an object was a HARD COMPILE +//! ERROR — `unsupported EIR backend feature: print_r for PHP type Object("V")` +//! — and an object nested inside an array rendered as NOTHING at all (tag 6 fell +//! through to `__rt_pr_val_done`). `top_level_object` and `object_in_array` are +//! those repros. +//! - EXPECTATIONS ARE REFERENCE PHP'S OUTPUT, BYTE FOR BYTE, taken from PHP +//! 8.4.20 (`php -d xdebug.mode=off`) on the same program. +//! - THE INDENT RULE PHP USES, and what these tests pin: `print_hash(indent)` +//! writes `(` and `)` at `indent`, entry lines at `indent + 4`, and renders a +//! value with `indent + 8`. A container value therefore closes with its own +//! `)\n` and the OUTER walker adds the per-entry `\n`, which is where PHP's +//! blank line after a nested `)` comes from. `nested_object_in_object` and +//! `array_property` are the shape tests for that. +//! - Enum cases print `E Enum` / `E Enum:int` / `E Enum:string` — a DIFFERENT +//! header from `var_dump`'s `enum(E::C)` — with `name` before `value`. elephc +//! stores a backed enum with `value` first, so the display order is fixed in the +//! descriptor (`hoist_enum_name_row`), which `backed_enum_case` pins. +//! - Tests invoke the elephc CLI (CARGO_BIN_EXE_elephc) as a subprocess in an +//! isolated temp dir, compile a plain executable, run it, and assert stdout — +//! the same harness style as `var_dump_object_tests`. Host-target only. +//! - Compile STDERR is filtered to elephc's OWN diagnostics: on Linux, GNU `ld` +//! adds static-glibc and `.note.GNU-stack` warnings that Apple's linker never +//! emits, so an unfiltered assertion would be non-portable. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static TEST_ID: AtomicUsize = AtomicUsize::new(0); + +/// Creates an isolated temp dir unique across parallel test threads/processes. +fn make_test_dir(prefix: &str) -> PathBuf { + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let tid = std::thread::current().id(); + let pid = std::process::id(); + let dir = std::env::temp_dir().join(format!("{}_{}_{:?}_{}", prefix, pid, tid, id)); + fs::create_dir_all(&dir).unwrap(); + dir +} + +/// Resolves the elephc CLI binary path (cargo env var, fallback next to the test binary). +fn elephc_bin() -> String { + std::env::var("CARGO_BIN_EXE_elephc").unwrap_or_else(|_| { + let mut path = std::env::current_exe().expect("failed to resolve current test binary"); + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path.join("elephc").to_string_lossy().into_owned() + }) +} + +/// Keeps only elephc's own diagnostics from a compile's stderr. +/// +/// Linking also surfaces the HOST linker's warnings, which are environmental +/// rather than anything elephc emitted: GNU `ld` reports static-glibc notes and +/// the `.note.GNU-stack` deprecation, while Apple's linker stays silent. +fn elephc_diagnostics(stderr: &str) -> String { + stderr + .lines() + .filter(|line| { + line.starts_with("Warning: ") + || line.starts_with("warning:") + || line.starts_with("warning[") + }) + .collect::>() + .join("\n") +} + +/// Compiles `source`, runs the executable and returns its STDOUT. +/// +/// Asserts a clean compile and a clean exit first: an object walker that reads a +/// wrong-shaped slot shows up as a signal rather than as bad text, and an +/// unguarded recursive walker blows the stack instead of producing a wrong string, +/// so the status assertions are load-bearing. +fn run_php(stem: &str, source: &str) -> String { + let dir = make_test_dir("elephc_print_r_object"); + let php = dir.join(format!("{}.php", stem)); + fs::write(&php, source).unwrap(); + + let mut cmd = Command::new(elephc_bin()); + cmd.env("XDG_CACHE_HOME", dir.join("cache-root")); + cmd.current_dir(&dir); + cmd.arg("-q"); + cmd.arg(&php); + let compile = cmd.output().expect("failed to spawn elephc"); + let raw_stderr = String::from_utf8_lossy(&compile.stderr).into_owned(); + assert!( + compile.status.success(), + "elephc compile failed:\n{raw_stderr}" + ); + let diagnostics = elephc_diagnostics(&raw_stderr); + assert!( + diagnostics.is_empty(), + "unexpected elephc diagnostics:\n{diagnostics}" + ); + + let out = run_binary(&dir.join(stem)); + let _ = fs::remove_dir_all(&dir); + out +} + +/// Runs a compiled executable and returns its STDOUT, asserting a clean exit. +fn run_binary(bin: &Path) -> String { + let output = Command::new(bin).output().expect("failed to run compiled binary"); + assert!( + output.status.success(), + "compiled binary exited non-zero ({:?}):\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// The headline repro: `print_r(new V())` was a hard compile error. +/// +/// Also pins the mixed-scalar body — a null-valued property renders as the empty +/// string after `=> `, which leaves a TRAILING SPACE on that line in PHP. +#[test] +fn top_level_object() { + let out = run_php( + "pr_top_level_object", + concat!( + " 1\n", + " [s] => hi\n", + " [f] => 1.5\n", + " [b] => 1\n", + " [n] => \n", + ")\n", + ) + ); +} + +/// An object with NO declared properties. Guards the zero-row walk: PHP still +/// writes both paren lines. +#[test] +fn empty_object() { + let out = run_php( + "pr_empty_object", + " 1\n", + " [b:protected] => 2\n", + " [c:P:private] => 3\n", + ")\n", + ) + ); +} + +/// An UNINITIALIZED typed property is OMITTED from `print_r` entirely — unlike +/// `var_dump`, which lists it as `uninitialized(int)`. +#[test] +fn uninitialized_typed_property_is_omitted() { + let out = run_php( + "pr_uninitialized", + " 1\n)\n"); +} + +/// The second repro: an object nested in an INDEXED array rendered as nothing. +/// +/// Pins PHP's nesting layout — the header follows `=> ` on the entry line, the +/// parens sit at `entry indent + 4`, and the entry's own `\n` after the nested +/// `)\n` produces the blank line. +#[test] +fn object_in_array() { + let out = run_php( + "pr_object_in_array", + concat!( + " new V()]);\n", + ), + ); + assert_eq!( + out, + concat!( + "Array\n", + "(\n", + " [0] => 0\n", + " [k] => V Object\n", + " (\n", + " [x] => 1\n", + " )\n", + "\n", + ")\n", + ) + ); +} + +/// An object property holding another OBJECT: two levels of the same frame. +#[test] +fn nested_object_in_object() { + let out = run_php( + "pr_nested_object", + concat!( + "inner = new V(); } }\n", + "print_r(new Outer());\n", + ), + ); + assert_eq!( + out, + concat!( + "Outer Object\n", + "(\n", + " [inner] => V Object\n", + " (\n", + " [x] => 1\n", + " )\n", + "\n", + ")\n", + ) + ); +} + +/// An object property holding an ARRAY: the other direction of the same nesting, +/// proving the object walker hands the array walker the right base indent. +#[test] +fn array_property() { + let out = run_php( + "pr_array_property", + " Array\n", + " (\n", + " [0] => 1\n", + " [1] => 2\n", + " [2] => 3\n", + " )\n", + "\n", + ")\n", + ) + ); +} + +/// A PURE enum case prints `E Enum` with only its `name`. +#[test] +fn pure_enum_case() { + let out = run_php( + "pr_enum_pure", + " Active\n)\n"); +} + +/// A BACKED enum case prints the backing type in the header and `name` BEFORE +/// `value`. elephc's storage order is the opposite, so this pins the display +/// reordering rather than an accident of layout. +#[test] +fn backed_enum_case() { + let out = run_php( + "pr_enum_backed", + concat!( + " Hearts\n", + " [value] => H\n", + ")\n", + "Lvl Enum:int\n", + "(\n", + " [name] => Low\n", + " [value] => 3\n", + ")\n", + ) + ); +} + +/// `print_r($o, true)` return mode: the same bytes, handed back as a string +/// instead of written to stdout. The length is asserted too, so a capture buffer +/// that truncated or over-copied could not pass on the text alone. +#[test] +fn return_mode_captures_the_object_body() { + let out = run_php( + "pr_object_return_mode", + concat!( + " 1\n", + " [b:protected] => 2\n", + " [c:P:private] => 3\n", + ")\n", + ) + ); +} + +/// A SELF-REFERENTIAL object renders ` *RECURSION*` after the header instead of +/// recursing forever. PHP writes the class name and ` Object\n` FIRST, then the +/// marker, and the entry line's own `\n` terminates it. +#[test] +fn self_reference_renders_the_recursion_marker() { + let out = run_php( + "pr_recursion", + concat!( + "self = $r;\n", + "print_r($r);\n", + ), + ); + assert_eq!( + out, + concat!( + "R Object\n", + "(\n", + " [a] => 1\n", + " [self] => R Object\n", + " *RECURSION*\n", + ")\n", + ) + ); +} + +/// TWO SIBLING references to the same instance must BOTH render in full: PHP +/// marks an object only for the duration of its own body, so a guard that pushed +/// and never popped would turn the second one into `*RECURSION*`. +#[test] +fn sibling_references_both_render_in_full() { + let out = run_php( + "pr_siblings", + concat!( + " V Object\n", + " (\n", + " [x] => 1\n", + " )\n", + "\n", + " [1] => V Object\n", + " (\n", + " [x] => 1\n", + " )\n", + "\n", + ")\n", + ) + ); +} diff --git a/tests/print_r_return_mode_heap_tests.rs b/tests/print_r_return_mode_heap_tests.rs index 711accbced..e1a339565a 100644 --- a/tests/print_r_return_mode_heap_tests.rs +++ b/tests/print_r_return_mode_heap_tests.rs @@ -285,3 +285,52 @@ echo $total, "\n"; let expected = format!("{}\n", 39 * ITERATIONS); assert_output_and_no_leak("var_export_return_loop", &source, &expected); } + +/// OBJECT COVERAGE for BUG A's fix: `print_r($object, true)` in a loop. +/// +/// `print_r` of an object only became possible at all with the C5 fix — it used to be the hard +/// compile error `unsupported EIR backend feature: print_r for PHP type Object("…")` — so this +/// is the first shape that exercises `__rt_print_r_object` through the CAPTURE buffer rather +/// than stdout. Nesting an object and an array inside the object makes the walk recurse through +/// `__rt_print_r_value` in both directions, and the loop separates a per-call capture-string +/// leak from the one live result the program still owns at exit. +/// +/// Reference PHP 8.4.20 renders 224 bytes for this object. +#[test] +fn print_r_object_return_mode_loop_frees_every_rendered_string() { + let source = format!( + r#"o = new Foo; }} }} +$total = 0; +for ($i = 0; $i < {ITERATIONS}; $i++) {{ + $s = print_r(new Bar, true); + $total += strlen($s); +}} +echo $total, "\n"; +echo $s; +"# + ); + let expected = format!( + concat!( + "{}\n", + "Bar Object\n", + "(\n", + " [o] => Foo Object\n", + " (\n", + " [a] => 1\n", + " [b] => xyz\n", + " [arr] => Array\n", + " (\n", + " [0] => 1\n", + " [1] => 2\n", + " )\n", + "\n", + " )\n", + "\n", + ")\n", + ), + 224 * ITERATIONS + ); + assert_output_and_no_leak("print_r_object_return_loop", &source, &expected); +} diff --git a/tests/var_dump_object_tests.rs b/tests/var_dump_object_tests.rs index 7ea4431e89..8d0166b6a0 100644 --- a/tests/var_dump_object_tests.rs +++ b/tests/var_dump_object_tests.rs @@ -1574,3 +1574,146 @@ fn computed_debug_info_is_ignored_and_declared_properties_print_instead() { ) ); } + +// --------------------------------------------------------------------------- +// ENUM CASES — `var_dump()` renders `enum(E::C)`, never an object body. +// +// REGRESSION ANCHOR (issue B16): every enum case used to fall through the tag-6 +// object path and print its backing storage, e.g. +// `object(Status)#1 (1) { ["name"]=> string(6) "Active" }` for a pure enum and a +// two-property body with `value` FIRST for a backed one. PHP prints one line: +// `enum(Status::Active)`. +// +// The test is at the value level, not the builtin level: `__rt_vd_val_obj` +// consults `__rt_obj_enum_name_offset` before writing anything object-shaped, so +// a nested enum gets the same treatment at any depth and at the right indent. +// Every expectation below is reference PHP 8.4.20's output for the same program. +// --------------------------------------------------------------------------- + +/// A PURE enum case at top level. PHP: `enum(Status::Active)`. +#[test] +fn top_level_pure_enum_case() { + let out = run_php( + "vd_enum_pure", + " Suit::Hearts]);\n", + ), + ); + assert_eq!( + out, + concat!( + "array(2) {\n", + " [0]=>\n", + " enum(Status::Active)\n", + " [\"k\"]=>\n", + " enum(Suit::Hearts)\n", + "}\n", + ) + ); +} + +/// An enum case held in an OBJECT PROPERTY. The property line indents like any +/// other value line, and the sibling `int` property proves the walk continues. +#[test] +fn enum_case_in_an_object_property() { + let out = run_php( + "vd_enum_in_object", + concat!( + "e = Status::Active; }\n", + "}\n", + "var_dump(new H());\n", + ), + ); + assert_eq!( + out, + concat!( + "object(H)#1 (2) {\n", + " [\"e\"]=>\n", + " enum(Status::Active)\n", + " [\"n\"]=>\n", + " int(7)\n", + "}\n", + ) + ); +} + +/// `var_dump()` is variadic: several values in ONE call, enums mixed with scalars, +/// each dumped independently in source order. +#[test] +fn several_values_in_one_var_dump_call() { + let out = run_php( + "vd_enum_variadic", + concat!( + " 1,\n 'b' => 'x',\n 'c' => 1.5,\n 'd' => true,\n))\n" + ); +} + +/// `stdClass` is the ONE class PHP exports as a cast instead of `__set_state`, +/// because it has no such method. An empty one still writes both lines. +#[test] +fn var_export_of_stdclass_renders_an_object_cast() { + let dir = make_test_dir("var_export_stdclass"); + let src = "o = new Foo; } } \ + var_export(new Bar); echo \"\\n\";"; + let bin = compile(&dir, src, "object_nested"); + assert_eq!( + run_binary(&bin), + concat!( + "\\Bar::__set_state(array(\n", + " 'o' => \n", + " \\Foo::__set_state(array(\n", + " 'a' => 1,\n", + " )),\n", + " 'arr' => \n", + " array (\n", + " 0 => 1,\n", + " 1 => 2,\n", + " ),\n", + "))\n", + ) + ); +} + +/// An object inside an ARRAY: the array branch must give an object value the same +/// `\n` + pad prefix it already gave a nested array, or the `\Foo::` would land on +/// the key line. +#[test] +fn var_export_of_objects_inside_an_array() { + let dir = make_test_dir("var_export_object_in_array"); + let src = " new stdClass]); echo \"\\n\";"; + let bin = compile(&dir, src, "object_in_array"); + assert_eq!( + run_binary(&bin), + concat!( + "array (\n", + " 0 => \n", + " \\Foo::__set_state(array(\n", + " 'a' => 1,\n", + " )),\n", + " 'k' => \n", + " (object) array(\n", + " ),\n", + ")\n", + ) + ); +} + +/// An ENUM case exports as the parsable constant expression `\Enum::Case`, with +/// no body at all, for pure and backed enums alike. +#[test] +fn var_export_of_enum_cases_renders_the_case_constant() { + let dir = make_test_dir("var_export_enum"); + let src = " Status::Active]); echo \"\\n\";"; + let bin = compile(&dir, src, "enum_cases"); + assert_eq!( + run_binary(&bin), + concat!( + "\\Status::Active|\\Suit::Hearts|\\Lvl::Low\n", + "array (\n", + " 'e' => \n", + " \\Status::Active,\n", + ")\n", + ) + ); +} + +/// `var_export` prints the BARE property name for a protected/private property — +/// no `:protected` / `:C:private` annotation, unlike `print_r` — and OMITS an +/// uninitialized typed property entirely. +#[test] +fn var_export_prints_bare_property_names_and_skips_uninitialized_ones() { + let dir = make_test_dir("var_export_visibility"); + let src = " 1,\n", + " 'b' => 2,\n", + " 'c' => 3,\n", + "))\n", + "\\U::__set_state(array(\n", + " 'd' => 1,\n", + "))\n", + ) + ); +} + +/// Return mode over an object: the same bytes handed back as a `string`, with the +/// length pinned so a truncated or over-copied render could not pass on the text +/// alone. Reference PHP 8.4.20 reports 64. +#[test] +fn var_export_object_return_mode_is_a_string_of_the_same_bytes() { + let dir = make_test_dir("var_export_object_return"); + let src = " 1,\n", + " 'b' => 2,\n", + " 'c' => 3,\n", + "))\n", + ) + ); +} + +/// LEAK GUARD for the object branch: `__elephc_object_prop_value` hands back a +/// FRESHLY boxed Mixed cell for every property, so exporting objects in a loop +/// must end with nothing live. Handing back the property's own cell instead would +/// alias object storage into a caller-released temporary; boxing without the +/// `Fresh` ownership declaration would leak one cell per property per call. +#[test] +fn var_export_object_loop_leaves_no_live_heap_blocks() { + let dir = make_test_dir("var_export_object_heap"); + let src = "o = new Foo; } } \ + $total = 0; \ + for ($i = 0; $i < 8; $i++) { $total += strlen(var_export(new Bar, true)); } \ + echo $total, \"\\n\";"; + let bin = compile_with_flags(&dir, src, "object_heap", &["--heap-debug"]); + let (stdout, heap) = run_binary_with_heap_report(&bin); + // Reference PHP 8.4.20 renders 167 bytes for one `var_export(new Bar, true)`. + assert_eq!(stdout, format!("{}\n", 8 * 167)); + assert!( + heap.contains("leak summary: clean"), + "var_export of objects leaked heap blocks:\n{heap}" + ); +} + +/// LEAK GUARD for the STRING paths, which leaked long before objects existed. +/// +/// `__elephc_var_export_escape` used to take `mixed $s` and cast it inside. Passing a `string` +/// value to a `mixed` parameter BOXES it into a fresh Mixed cell that nothing releases, so every +/// exported string value and every exported string KEY leaked one heap block — in every program +/// that called `var_export` on anything containing a string. The helper now takes `string` and +/// each caller casts into a `string` local first, so no box is created at the call boundary. +/// +/// This is deliberately separate from the object leak guard: it fails on a plain array and does +/// not need an object at all. +#[test] +fn var_export_string_loop_leaves_no_live_heap_blocks() { + let dir = make_test_dir("var_export_string_heap"); + let src = " 'zz'], true)); \ + } \ + echo $total, \"\\n\";"; + let bin = compile_with_flags(&dir, src, "string_heap", &["--heap-debug"]); + let (stdout, heap) = run_binary_with_heap_report(&bin); + // Reference PHP 8.4.20: `'zz'` is 4 bytes, `array (\n 'k' => 'zz',\n)` is 24. + assert_eq!(stdout, format!("{}\n", 8 * (4 + 24))); + assert!( + heap.contains("leak summary: clean"), + "var_export of strings leaked heap blocks:\n{heap}" + ); +}