The Runtime
Hand-written assembly routines for strings, arrays, generators, fibers, system calls, exceptions, and I/O.
Source: src/codegen_support/runtime/: mod.rs, emitters.rs, diagnostics.rs, data/, strings/, arrays/, buffers/, callables/, compare/, exceptions.rs, exceptions/, io/, objects/, spl/, system/, pointers/, zval/, fibers/, generators/, and the target-aware bcmath/, curl/, pdo/, and eval bridge modules
The runtime is a collection of hand-written assembly routines that handle operations too complex for inline code generation. When the code generator needs to convert an integer to a string or concatenate two strings, it emits a bl __rt_itoa or bl __rt_concat — a call to a runtime routine.
These routines are usually pre-assembled into the cached runtime object rather
than textually appended to each user .s file. Feature gates omit optional
families, and executable linking dead-strips unreachable helpers, so the final
binary contains only the required subset. They remain part of that binary
rather than an external shared dependency.
Eval execution paths
eval() is an optional hybrid boundary rather than part of the ordinary
shared runtime. Eligible literal fragments are parsed at compile time and
lowered directly to EIR. Some literal fragments use only the core eval-scope
helpers, while dynamic or unsupported fragments link the
elephc_magician static interpreter bridge.
The bridge shares elephc’s boxed Mixed cells, copy-on-write containers,
generated class/callable metadata, diagnostics, exceptions, and target-aware
ABI helpers. It is embedded only when the final EIR module requires the
eval_bridge runtime feature; the presence of a fully native literal
eval() call does not force it into the binary.
Magician’s base archive does not reference PCRE2. When static detection or
--with-regex enables the regex runtime, generated eval setup registers the
managed PCRE2 shim as an opaque provider; otherwise regex builtins are absent
from dynamic eval dispatch.
The compile-time planner, scope/context lifecycle, eval-specific EIR instructions, parse cache, ownership rules, and bridge linking contract are documented in Eval Runtime Architecture. PHP-visible syntax, supported statements and builtins, reflection behavior, safety, and limitations live in Eval.
Why a runtime?
Some operations can’t be done with a few inline instructions:
- Integer to string (
itoa): Requires a loop that divides by 10, extracts digits, and writes them right-to-left - String concatenation: Needs to copy bytes from two source strings into a buffer
- Array operations: Require heap allocation, bounds checking, and element copying
These are 20-50+ instructions each. Inlining them at every call site would bloat the binary. Instead, they’re emitted once and called with bl.
Naming convention
All runtime routines start with __rt_:
__rt_itoa integer → string
__rt_resource_to_string resource → "Resource id #N"
__rt_ftoa float → string
__rt_concat string + string → string
__rt_str_append consuming append onto a local string owner
__rt_str_eq string == string → bool
__rt_array_new allocate a new array
__rt_throw_current throw the active exception
__rt_build_argv build $argv from C strings
Extending a routine’s ABI without touching its callers
A routine that gains an argument keeps its original label and original ABI, and gains a SIBLING that takes the new one. The original sets the defaults and branches to it, so the two share one body rather than two that have to be kept in step:
__rt_file_put_contents flags = 0 → __rt_file_put_contents_flagged
__rt_file_put_contents_maybe_phar → …_maybe_phar_flagged
__rt_mkdir 0777, false → __rt_mkdir_ex
The reason is that runtime routines have callers inside the runtime, not only from lowering. Widening an ABI in place would mean auditing every one of them for a register it has never set — and a tail call that forwards a stale register fails silently rather than loudly. Pairing leaves that audit undone because it is unnecessary.
The two labels end up with different caller sets, and which one a site uses says which ABI it depends on:
| callers inside the runtime | |
|---|---|
__rt_file_put_contents | 4 — the phar archive writer and copy(), on each target |
__rt_file_put_contents_flagged | 2 — the maybe_phar plain path on each target, tail-calling with the flags it was handed |
Reading that table the other way is how to tell whether a change to one entry point can reach the other’s callers.
The pairs are what let file_put_contents() take PHP’s $flags and mkdir()
take $permissions / $recursive while every existing call site emits exactly
the instructions it did before (issue #506).
Two consequences worth knowing when adding one:
- The entry point must BRANCH to the sibling rather than fall through to it. On macOS the dead-strip pass treats each global label as its own atom, so two globals in one block can be separated by the linker.
- The sibling’s extra argument register must miss everything the body already
uses, including registers a nested helper clobbers.
__rt_file_put_contentstakes its flags inx5/r8, and the phar gate’sr8is why the flag is read there before the bridge arguments are arranged.
Diagnostic routines
Source: src/codegen_support/runtime/diagnostics.rs
These helpers implement PHP’s @ error-suppression operator and the runtime warning channel. The suppression depth lives in _rt_diag_suppression; while it is non-zero, suppressible warnings are silently dropped instead of written to stderr. They are emitted before any PHP-visible helper so the rest of the runtime can report warnings through a single path.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_diag_push_suppression | Enter one nested @ suppression scope (increment _rt_diag_suppression) | — | — |
__rt_diag_pop_suppression | Leave one @ suppression scope, clamped against underflow | — | — |
__rt_diag_warning | Write a runtime warning string to stderr unless suppression is active | x1/x2 = message string | — |
Array-key conversion uses __rt_float_key_to_int to apply PHP’s integer
conversion and report precision loss or an unrepresentable float through the
diagnostic channel. A single PHP array access can probe, insert, and probe the
same key again. Only its first conversion emits diagnostics; subsequent probes
use __rt_php_float_to_int to obtain the same integer key without repeating the
message. This applies to typed float keys and float values inside Mixed cells.
Out-of-bounds string reads use __rt_warn_string_offset to report
the missing offset. Silent existence probes such as isset() and ?? do not
call that warning helper. A float string offset instead reports String offset cast occurred before truncating the offset, including for integral-valued
floats. These warnings follow @ suppression and the registered PHP error
handler through the shared diagnostic dispatcher.
Boxed float offsets use the same warning before the string fetch.
Integer-form string offsets remain silent, including when their value is boxed.
String routines
Source: src/codegen_support/runtime/strings/
__rt_itoa — Integer to string
File: strings/itoa.rs
Converts a signed 64-bit integer in x0 to a decimal string.
Input: x0 = integer value
Output: x1 = pointer to string, x2 = length
Algorithm:
- Check for negative → set flag, negate
- Check for zero → output “0” directly
- Loop: divide by 10 (
udiv+msub), convert remainder to ASCII digit (+ 48), store right-to-left - Prepend ’-’ if negative
- Update concat buffer offset
The digits are written right-to-left because division gives us the least significant digit first. The result is written into the concat buffer.
__rt_itoa_into formats the same signed integer directly into a caller-owned
21-byte window, returning its right-aligned pointer/length slice without changing
_concat_buf or _concat_off. AArch64 takes the integer in x0 and destination
in x1, returning pointer/length in x0/x1; x86_64 takes rdi/rsi and
returns rax/rdx. implode() uses this path after reserving its destination so
integer formatting cannot overwrite its live scratch-buffer output.
__rt_resource_to_string - Resource to string
File: strings/resource_to_string.rs
Formats the native resource payload used by stream handles as PHP’s display string (Resource id #N). The helper keeps resources distinct from integers when boxing into mixed, while still letting the I/O runtime pass the underlying file descriptor to stream syscalls.
Input: x0 = native resource payload
Output: x1 = pointer to string, x2 = length
__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 (precision = 14)
File: strings/ftoa.rs
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
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.
__rt_ftoa_into uses the same formatter with a caller-provided destination
(x1 on AArch64, rsi on x86_64), preserving the shared concat cursor. It returns
the usual string pointer/length pair. Both entry points branch to
__rt_ftoa_body, a global shared body whose symbol keeps it reachable when
Mach-O dead stripping separates the two entry points into distinct atoms.
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
__rt_concat — String concatenation
File: strings/concat.rs
Concatenates two strings by copying both into the concat buffer.
Input: x1/x2 = left string (ptr/len), x3/x4 = right string (ptr/len)
Output: x1 = pointer to result, x2 = total length
Algorithm:
- Get current position in concat buffer
- Copy left string bytes (byte-by-byte loop)
- Copy right string bytes
- Update buffer offset
- Return pointer to start of result + total length
__rt_str_append — Consuming string append
File: strings/str_append.rs, with the bodies in strings/str_append/aarch64.rs and strings/str_append/x86_64.rs
Appends a borrowed right-hand string to a string owner that EIR has already decided to consume. Lowering calls it for an admitted StrAppend; ordinary StrConcat still calls __rt_concat. The helper uses the same pointer/length ABI as concat and does not allocate concat scratch. The header’s size field is the payload capacity. The returned pair’s length word is the logical length.
Input: AArch64 x1/x2 = left owner (ptr/len), x3/x4 = borrowed right-hand string (ptr/len). x86_64 rax/rdx = left owner, rdi/rsi = borrowed right-hand string. Every AArch64 target, including iOS device and Simulator, uses the AArch64 body. Linux x86_64 uses the x86_64 body.
Output: AArch64 x1/x2, x86_64 rax/rdx = result pointer and logical length.
Algorithm:
- Add the two lengths before any payload access. Unsigned wrap, or a sum above
0xfffffff8(the largest aligned payload the heap header can represent), branches to__rt_alloc_overflow. That trampoline recovers through an active cdylib boundary; an ordinary executable writes the allocation-overflow fatal and exits. On the unique kind-1 path, a right-hand interval whose end address wraps takes the same branch before the overlap test. - A zero-length right-hand side returns the original pointer and length. Ownership and mbstring origin stay as they were.
__rt_heap_kindclassifies the left pointer. Kind 1 is an ordinary owned string. A literal, concat-scratch slice, or other non-kind-1 value is copied into a new allocation. This helper does not release that input.- A kind-1 string with refcount 1, whose right-hand side lies completely outside
[payload, payload + capacity), is reused when the new logical length fits in that capacity. Reuse keeps the same owner. The suffix write happens after the origin refresh below. - Otherwise it allocates before retiring any old owner. A unique buffer that is too small doubles its previous capacity. A shared string or an overlap, including appending a string to itself, copies instead, and that growth seed is the old logical length rather than unused spare capacity. A non-kind-1 input has no capacity to double, so the seed is zero. The reservation is the larger of that growth and the required length, and at least 16 bytes. When doubling would exceed
0xfffffff8, the reservation is the exact required length. The new block is stamped kind 1, with the x86_64"ELPH"marker in the kind word. - When the mbstring runtime feature or the eval bridge is enabled, a non-empty append calls
__rt_mbstring_ini_freshwith the result pointer and the new length before any result byte is stored, installing a fresh INI origin for the changed string. The call is omitted when those features are off. An empty append has already returned, so its origin is preserved either way. - A new allocation copies the old prefix, then the right-hand side, including embedded NUL bytes. Only after that copy does the helper
__rt_decref_anythe old kind-1 owner. Reused unique owners skip that release.__rt_heap_allocruns before the decref, so a failed allocation leaves the old local owner in place.
__rt_atoi — String to integer
File: strings/atoi.rs
Parses a decimal string into a 64-bit integer. Handles optional leading - sign.
Input: x1 = string pointer, x2 = length
Output: x0 = integer value
__rt_php_float_to_int and __rt_php_float_to_int_cap — the TWO double-to-int rules
File: numeric.rs
PHP has two different rules for turning a double into an int, and which applies depends on where the double came from. Both live here so neither is ever open-coded at a call site.
| source | rule | helper | (int) of 1e19 |
|---|---|---|---|
a float VALUE ((int)1e19) | reduce modulo 2^64 | __rt_php_float_to_int | -8446744073709551616 |
a numeric STRING ((int)"1e19") | CAP at the integer range | __rt_php_float_to_int_cap | 9223372036854775807 |
Both answers are what php-src produces; they differ because the string path runs through
strtol-style saturation while the value path is a C cast. __rt_str_to_int therefore calls
the capping helper, not the wrapping one — using the sibling turned (int)"1e19" into the
negative wrapped value.
The hardware conversion instructions disagree on non-finite input, and that disagreement is
the reason these are shared helpers at all: AArch64’s fcvtzs saturates by definition, while
x86_64’s cvttsd2si answers with the “integer indefinite” pattern 0x8000000000000000. A bare
conversion at a call site therefore made (int)NAN differ between targets. The two helpers
avoid it differently:
__rt_php_float_to_intnever uses those instructions. It decodes the IEEE-754 sign, exponent and significand with integer instructions and shifts the significand into place, so the modulo-2^64 reduction is exact and bit-identical on both architectures. NaN and the infinities carry the all-ones exponent, so they land in the same “every significand bit leaves the 64-bit window” arm that answers0.__rt_php_float_to_int_capis the only helper that converts withfcvtzs/cvttsd2si, and it checks for NaN and the infinities before converting, answering0for them on both architectures. Its remaining work is well defined: every value reachingfcvtzs/cvttsd2siis finite, so AArch64 saturates to exactly PHP’s cap and x86_64 only has to correct a positive overflow toPHP_INT_MAX.
Input: d0 / xmm0 = source double
Output: Both helpers return the PHP integer value in x9 / r11.
__rt_str_eq — String equality
File: strings/str_eq.rs
Compares two strings byte-by-byte.
Input: x1/x2 = first string, x3/x4 = second string
Output: x0 = 1 if equal, 0 if not
Algorithm:
- Compare lengths — if different, return 0 immediately
- Loop: compare byte by byte
- If all bytes match, return 1
Other string routines
Each routine follows the same pattern — inputs in registers, output in standard result registers:
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_strcopy | Copy string into concat buffer | x1/x2 | x1/x2 |
__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) |
__rt_strtolower | Lowercase conversion | x1/x2 | x1/x2 |
__rt_strtoupper | Uppercase conversion | x1/x2 | x1/x2 |
__rt_trim | Strip whitespace (no args) or chars in mask | x1/x2 | x1/x2 |
__rt_ltrim / __rt_rtrim | Strip left/right whitespace or mask | x1/x2 | x1/x2 |
__rt_trim_mask | Strip chars in custom mask from both ends | x1/x2 + mask | x1/x2 |
__rt_ltrim_mask / __rt_rtrim_mask | Strip custom mask from left/right | x1/x2 + mask | x1/x2 |
__rt_strrev | Reverse string (byte-wise) | x1/x2 | x1/x2 |
__rt_grapheme_strrev | Reverse a UTF-8 string by grapheme cluster for PHP 8.6 grapheme_strrev(); returns false on malformed UTF-8 | x1/x2 | x1/x2 |
__rt_mbstring_native | Shared invocation adapter that propagates failures through ordinary exception unwinding | C ABI operation ID, extended descriptor-pointer array, count, caller strictness, optional eval context | Primary value, status, byte length, result kind |
__rt_mbstring_status | Same owned results and errors, with pending exceptions returned to eval without unwinding | C ABI operation ID, argument pointer, count | AArch64 x0/x1/x2/x3; x86_64 rax/rdx/rcx/r8 |
__rt_mbstring_string_array | Validates packed binary strings and transfers copied elements into an owned indexed array of Mixed cells | C ABI packed pointer, byte length, element count | Owned array pointer, or zero for invalid framing |
__rt_mbstring_array_next | Borrows one ordered native array entry and normalizes concrete value tags without allocation or mutation | C ABI context, array descriptor, cursor, key output, value output | Entry, end, or failure status |
__rt_mbstring_array_argument | Snapshots a borrowed native graph and copies its wire buffer into one owned runtime string | Concrete array tag and native payload identity | AArch64 x1/x2; x86_64 rax/rdx owned wire bytes |
__rt_mbstring_release_array_arguments | Consumes packed-array argument owners and clears their byte pointers before diagnostics or exception handling | C ABI mutable argument slots and count | Released array slots; ordinary strings retain caller ownership |
__rt_mbstring_box_result | Boxes integer, boolean, string, or array results and releases intermediate heap ownership | Successful value/status/length/kind tuple | Fresh Mixed cell shared by AOT and eval |
__rt_mbstring_stringable | Protects native/eval string conversion with a PHP exception boundary | C ABI eval context, borrowed boxed object, MbHostStringV1 output | Runtime status and native string owner; zero output on failure |
__rt_mbstring_capture_hash_store | Internal construction primitive: persist one capture, protect old-value release, track reentrant hash set/unset, and finish the selected write without COW | C ABI unused context, selected managed hash, validated borrowed key/value descriptors | Status 0 or pending throwable 2 |
__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) |
__rt_implode | Join string array with glue | glue + array | x1/x2 |
__rt_implode_int | Join integer array with glue | glue + array | x1/x2 |
__rt_implode_float | Join float array with glue, formatting each element through __rt_ftoa | glue + array | x1/x2 |
__rt_implode_bool | Join bool array with glue (true as 1, false as the empty string) | glue + array | x1/x2 |
__rt_strcmp | Binary comparison | two strings | x0 (-1, 0, 1) |
__rt_str_smart_cmp | PHP string ordering: compare integer strings exactly, apply PHP’s numeric-overflow rules, and use byte ordering for nonnumeric pairs | x1/x2 = left slice, x3/x4 = right slice | x0 (-1, 0, 1) |
__rt_str_numeric_ex | Classify a complete PHP string as numeric, rejecting embedded NUL bytes and retaining exact integer and overflow metadata | x1/x2 = string slice | x0 = numeric flag, x1 = integer-kind flag, x2 = overflow sign, x3 = exact integer, d0 = double |
__rt_num_run_class | Classify a clipped numeric run as integer or double without losing 64-bit integer precision | x0 = NUL-terminated numeric run | x0 = integer-kind flag, x1 = overflow sign, x2 = exact integer |
__rt_strcasecmp | Case-insensitive compare | two strings | x0 |
__rt_str_starts_with | Check prefix match | x1/x2 + x3/x4 | x0 (0 or 1) |
__rt_str_ends_with | Check suffix match | x1/x2 + x3/x4 | x0 (0 or 1) |
__rt_chr | ASCII code → char | x0 | x1/x2 |
__rt_addslashes | Escape quotes/backslashes | x1/x2 | x1/x2 |
__rt_nl2br | Insert <br /> before newlines | x1/x2 | x1/x2 |
__rt_bin2hex | Binary → hex string | x1/x2 | x1/x2 |
__rt_hex2bin | Hex → binary | x1/x2 | x1/x2 |
__rt_md5 | MD5 hash | x1/x2 | x1/x2 |
__rt_sha1 | SHA1 hash | x1/x2 | x1/x2 |
__rt_sprintf | Format string, including deferred static/Mixed coercion and eval-aware object stringification | format + tagged args on stack + optional eval context | x1/x2 |
__rt_sprintf_pack_mixed | Pack a boxed scalar or preserve a boxed non-scalar for deferred formatting | boxed Mixed in x0 | record payload/tag in x0/x1 |
__rt_sprintf_mixed_to_int | Apply PHP numeric formatting rules and warnings to a boxed or raw-tagged array, object, callable, iterable, or resource | record tag/payload + conversion kind + optional eval context in x0-x3 | x0 |
__rt_sprintf_mixed_to_string | Render a boxed or raw-tagged array/resource, dispatch native/eval __toString(), or throw a catchable Error for a non-stringable value | record tag/payload + optional eval context in x0/x1/x2 | owner + x1/x2 |
__rt_base64_encode | Base64 encode | 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 honouring the ENT_* quote and document-type flags | x1/x2 + flags in x3 | x1/x2 |
__rt_html_entity_decode | Decode HTML entities | x1/x2 | x1/x2 |
__rt_strip_tags | Strip HTML and PHP tags, preserving allowed HTML tags while always removing comments and PHP tags | x1/x2 = subject pointer/length, x3/x4 = allow-list pointer/length | x1/x2 = stripped string |
__rt_rawurlencode | URL encode (RFC 3986) | x1/x2 | x1/x2 |
__rt_parse_url | Parse URL bytes and select an array/string/int/null/false result | x1/x2 + component in x3 | x0 (Mixed ptr) |
__rt_stripslashes | Remove escape backslashes | x1/x2 | x1/x2 |
__rt_ucwords | Uppercase first letter of each word | x1/x2 | x1/x2 |
__rt_str_ireplace | Case-insensitive replace | search + replace + subject | x1/x2 |
__rt_substr_replace | Replace substring at offset | str + replacement + start + len | x1/x2 |
__rt_str_pad | Pad string to length | str + len + pad_str + type | x1/x2 |
__rt_str_split | Split into chunks | str + chunk_len | x0 (array ptr) |
__rt_wordwrap | Wrap text at word boundaries | str + width + break + cut | x1/x2 |
__rt_number_format | Format number with separators | float + decimals + sep | x1/x2 |
__rt_hash | Hash with algorithm | algo + data | x1/x2 |
__rt_hash_init / __rt_hash_update / __rt_hash_final | Incremental hash-context API backing hash_init() and friends | context + data | context / x1/x2 |
__rt_hash_copy | Clone an incremental hash context | context | context |
__rt_hash_ctx_free | Free a HashContext via elephc_crypto_free; the sole destructor, called by __rt_mixed_free_deep when a Mixed(tag=9, kind=2) cell is released at scope exit (hash_final no longer frees) | context | — |
__rt_hash_hmac | Keyed HMAC over a message | algo + key + data | x1/x2 |
__rt_hash_equals | Constant-time string comparison | two strings | x0 (0 or 1) |
__rt_hash_algos_list | Build the hash_algos() array of supported algorithm names | — | x0 (array ptr) |
__rt_digest_to_string | Format a raw digest as lowercase hex | digest | x1/x2 |
__rt_crc32 | CRC32 checksum | x1/x2 | x0 |
__rt_inet_ntop / __rt_inet_pton | IPv4/IPv6 address ↔ packed-binary conversion. IPv4 is parsed and rendered here (__rt_ip2long/__rt_long2ip); IPv6 goes through libc inet_pton(3)/inet_ntop(3), which own :: compression and the embedded-IPv4 form — and which disagree about zone identifiers (macOS accepts and ignores one, glibc refuses it), a split PHP inherits too. Both destinations come from __rt_concat_reserve and are published with __rt_concat_publish, so a 16-byte packed address or a 45-byte rendering cannot run off the end of the 64 KiB scratch buffer. The family is chosen the way php-src chooses it — a : in the text, the packed LENGTH on the way back — and AF_INET6 is the TARGET’s value (30 on Darwin, 10 on Linux), pinned by emitter tests because a wrong one turns every IPv6 address into false only on the cross-compiled binary | address | x1/x2 |
__rt_long2ip / __rt_ip2long | Dotted-quad string ↔ integer conversion | x0 or x1/x2 | x1/x2 or x0 |
__rt_vsprintf | vsprintf() formatting with an argument array | format + array + optional eval context | x1/x2 |
__rt_sscanf | Parse string with format | str + format | x0 (array ptr) |
The mbstring engine borrows string arguments and validated, pointer-free array
graphs through elephc_mbstring_call_v1. The separate
elephc_mbstring_snapshot_v1 entry copies host arrays before request-state dispatch.
Its nonmutating C reader runs outside the Rust request-state borrow, retains array
identity for cycles and aliases, and copies binary keys and values before the next
reader invocation. Rust does not interpret native array layouts. The target-aware
reader owns indexed strides, hash iteration, Mixed unboxing, and nullable-container
normalization.
The Rust encoding catalog is one immutable static slice. Codec selection and
encoding lookup share its metadata and embedded byte tables, rather than
instantiating the complete const catalog at multiple use sites. Regenerating
the catalog preserves that storage policy; the encoding order and payload bytes
still come from the PHP fixtures.
AOT and production eval calls use the shared invocation coordinator for argument
preparation and delayed array snapshots. AOT supplies stack-local borrowed invoker
descriptors containing each value’s concrete tag and raw payload. The separate
__rt_mbstring_status wire adapter still consumes owned runtime strings staged in
ARG_ARRAY slots and clears those slots after dispatch, including on failure.
The raw Rust C API
continues to borrow every input buffer and release only its own result buffers;
external C callers must keep their argument storage alive themselves.
mb_check_encoding uses this graph path for recursive validation and the same
request counter as mb_scrub for deprecated null/omitted-input checks.
mb_substitute_character preserves integer codepoints separately from string modes
through the same union-argument staging. Null/omitted input reads the current mode;
named modes retain the remembered replacement codepoint. A failed setter leaves
that request state unchanged.
Optional mbstring runtime emission also follows callable references. First-class
callable requirements and finite names proven at dynamic invocation sites enable
the same bridge as direct calls. Generic callback dispatch emits mbstring wrappers
only when that runtime is active, including through eval or --with-mbstring.
Opaque runtime names can use the explicit flag; unrelated callbacks do not force
mbstring into their runtime object or link.
Native eval scopes also omit mbstring dispatcher arms unless mbstring or the full
eval bridge is enabled, preserving links that need only native scope helpers.
The preparatory bridge entries elephc_mbstring_arity_v1 and
elephc_mbstring_prepare_v1 validate the outer call before request-state dispatch.
They derive arity, types, and parameter names from the shared contract. Preparation
receives a concrete host-value descriptor and caller strictness, then returns a
ready scalar, a TypeError, or a deferred host action for a borrowed string, array,
float formatter, or Stringable callback. It never traverses host arrays/objects
or borrows request state. Its result buffers use elephc_mbstring_release_v1.
Unlike ordinary operation diagnostics, preparation diagnostics are binary records
containing a little-endian level, byte length, and complete message. Hosts deliver
them in order before performing deferred actions and stop if a handler throws.
Production AOT and eval calls consume this preparation protocol through the shared
coordinator. Direct AOT calls supply physical-source strictness; dynamic callable
wrappers and eval fragment strictness still need complete propagation.
elephc_mbstring_invoke_v1 now provides the shared invocation coordinator for
this migration. Its original 72-byte V1 callback table supplies by-value copying,
concrete description, Stringable execution, float formatting, diagnostic delivery,
owner release, and nonmutating array iteration. The generated native/eval host
uses the 96-byte V3 extension. V2 adds an independently owned array-entry reader;
V3 adds graph identity reads and reference pinning without a by-value copy.
V1 callers remain supported for operations that need no incremental array-element
coercion. Encoding-list arrays and numeric-entity maps require the V2 reader. Arity is checked before reading
host inputs. All argument values are copied before coercion, each parameter’s
metadata and diagnostics are consumed in order, and arrays are snapshotted only
after all outer conversions finish. Only operation dispatch borrows request state.
Every acquired host owner is recorded before result validation; explicit cleanup
runs after success, failure, or a contained Rust panic, continuing after release
errors. Callbacks must contain PHP exceptions, and release callbacks must consume
owners even when destructors throw. The generated __rt_mbstring_invoke installs
real native callbacks, and all 58 shared AOT/eval operations use it with the
available caller context. __rt_mbstring_materialize transfers Rust result ownership into the
existing PHP result/error path, including ArgumentCountError for invalid arity.
The eval registry allows these calls to reach shared arity validation after
evaluating the supplied argument expressions.
The V5 host contract extends the V4 prefix with mb_parse_str configuration,
optional SAPI filtering, and ordered live query writes. Shared operation 82
copies and coerces the source, pins the required output independently, initializes
it, then snapshots mbstring encoding settings. Core configuration reads are
phased: entry reads separators and the whole-query variable limit, field reads
the nesting limit after filtering, and diagnostic reads display_errors after
the write and its destructors. The name planner supplies normalized enter,
store, and root-removal instructions; the host owns array storage, append
counters, nested COW, and protected destructor execution.
Pending exceptions do not prevent subsequent query writes or the final aggregate
input identification update. Diagnostics stop reaching user handlers while a
throwable is pending. The common arena releases filter/configuration leases,
writer ownership, argument copies, and reference pins on every exit. Independent
ABI tests compare parser results and live callback traces with PHP. Native/eval
query storage callbacks and public bindings remain pending, so both backend
contracts explicitly report ReferenceAdaptersPending for mb_parse_str.
The V4 capture-output contract now has direct AOT bindings for mb_ereg and
mb_eregi. Calls may omit the output or pass a managed Mixed local or alias.
Shared argument planning promotes the output at its source-order evaluation
point, including named arguments and conditional predecessor paths. The backend
requires proven tracked storage before recovering the reference wrapper; raw
by-reference parameters, property aliases, and dynamic spread references remain
unsupported in AOT. Its runtime-selected callable wrappers remain pending.
Eval bindings use the same V4 coordinator with persistent reference-wrapper
owners acquired during source-order argument evaluation. Local variables,
aliases, eval reference parameters, named dynamic calls, and explicitly
referenced unpacked elements retain the caller’s cell identity. Ordinary
arguments keep their independent value copies. Shared call-boundary cleanup
releases reference pins, argument owners, and inserted defaults on success or
failure. Direct call_user_func syntax evaluates independent values, warns
for the supplied output, and transfers its owner into a temporary wrapper.
Capture initialization can release the previous temporary value immediately,
including destructor reentry or a pending exception, without changing the
caller’s variable. The common owned-argument boundary also handles
call_user_func_array, nested callback wrappers, array_map, and reflected
builtin invocation. Explicit call-array references retain their live output
identity; ordinary output values warn and receive temporary reference cells.
Reflected invoke arguments use independent values, while invokeArgs can
carry persistent references. Native ReflectionFunction construction registers
the builtin name so invocation reaches its shared eval binding.
Direct call_user_func_array syntax consumes its temporary source array after
capturing argument owners and before invoking the callee. A caller’s variable
still keeps its own array owner. Dynamic callback wrappers instead keep their
ordinary argument owners until the outer call returns. This difference can
change a capture result: a temporary old object’s destructor may update regex
options during direct capture initialization, whereas the dynamic wrapper’s
array keeps that old object alive until matching has finished.
Computed callback expressions and dynamic callees own their selected values through invocation. Owned concatenation releases its source operands and Stringable conversions, and a full ternary releases its condition before evaluating the selected branch. The native concat wrapper persists each converted byte range before converting the next operand, then releases both conversion buffers after boxing the result. Numeric scratch buffers therefore cannot overwrite an earlier operand, and existing string casts do not leak. Property and direct array output destinations, non-reference unpacked outputs, and legacy native reference-slot adapters still need integration. The boxed dispatcher emits the capture coordinator call only when the managed regex runtime is active; it reports unsupported otherwise.
Validation-error cleanup currently releases a temporary callback output at
the call boundary. PHP can retain that object through exception trace arguments
when zend.exception_ignore_args=0, delaying its destructor until the throwable
is released. The callback cleanup regression uses PHP’s non-retaining trace
configuration (zend.exception_ignore_args=1) as its oracle. Retaining trace
arguments requires shared throwable ownership support and remains unverified.
__rt_mbstring_capture_native shares ordinary native mbstring exception
propagation and passes the live output identity to __rt_mbstring_capture_invoke.
Initialization and publication use the existing protected V4 coordinator.
During untyped initialization the reference can temporarily contain a null
child pointer. Eval replacement materializes that previous PHP null for its
owned-result cleanup contract, allowing a destructor to finish its assignment
and continue to later side effects or an exception. Eval closure parameter
checks also use the shared dynamic-object class relation, so an observer
closure satisfies its declared Closure constructor parameter.
The shared elephc_mbstring_capture_apply_v1 entry validates an
entire flat graph before invoking ordered per-entry callbacks, and continues
after pending destructor exceptions. The native __rt_mbstring_capture_hash_store
primitive takes an already selected hash, pins its lifetime without adding a PHP
COW owner, persists captured bytes, protects old-value release, and looks the key
up again after callbacks may have grown the entry allocation. It completes the
write before unpinning, including when that final release destroys the selected
array. Real PHP destructor/exception and heap-debug tests cover this path through
internal test shims for capture construction and reentrant hash mutations.
While releasing an overwritten value, the construction helper links a five-word
record (next, selected hash, key low/high, changed) through _hash_write_guard_top.
The hash pin and borrowed capture descriptor keep its hash and key identity valid.
Ordinary __rt_hash_set, __rt_hash_unset, and nested capture construction claim
the selected entry owner before releasing it. A matching active record with no
previous mutation marks the old payload as already owned by the outer release;
the writer skips that release and marks every matching record changed. The old
entry remains observable until a callback actually writes or removes it. After
protected destruction returns, construction unlinks its exact record, looks up
the key again, and consumes any callback-installed replacement before publishing
the final capture. Key comparison is binary-safe and separated hash identities
have independent records. No guard remains linked after an ordinary or pending
throwable return from this primitive.
__rt_hash_to_mixed participates in the same ownership protocol after COW.
Raw entries transfer their existing owner into a box, or acquire a new payload
owner when protected destruction already borrowed the old one. For an existing
Mixed entry, __rt_hash_write_guard_owns inspects the guard without changing it.
An owned box stays unchanged; a borrowed box is replaced with a cloned PHP value
through __rt_mixed_clone, including unwrapping nested reference cells. The old
boxes still belong to the suspended release, while the new box owns its retained
object payload. Repeated conversion leaves that new owner intact. Construction
then consumes any surviving replacement before publishing the final capture.
Native fixtures cover numeric/binary keys and raw, boxed, and nested-reference
receivers. PHP destructor tests cover raw and preboxed receivers, conversion
alone or followed by set/unset/nested capture, pending throws, and clean heaps.
Shallow hash copies use __rt_reference_array_copy for Mixed entries before
conversion. If the source box has zero owners because its protected child
release is active, this helper clones the PHP value rather than retaining the
old box. The independent hash keeps a valid object owner while the original
capture write completes, without changing the original hash’s release guard.
Tests release copies both inside the destructor and after reading retained
object properties from PHP, including pending exceptions and nested references.
Native request teardown now owns values displaced by reentrant initialization and releases them through protected cleanup. Its FIFO order follows adoption order, which can differ from PHP’s object creation order for nested captures; that shutdown ordering still needs integration. Direct slot writers, shared indexed destinations, nested containers with partially released children, and general resource/callable teardown also remain incomplete. Focused public AOT tests cover output identity, named inputs, conditional promotion, initialization errors, and alias-observing destructors with pending exceptions and clean heaps. Opaque eval tests cover the same initialization ordering and pending exception behavior, plus explicit unpacked references and stable retained allocation counts after repeated successful and failing calls. They do not establish the remaining reference forms or nested shutdown ordering.
Encoding-list arrays for mb_detect_order are read and converted one entry at a
time. Every entry is copied before its cast, so callbacks cannot invalidate that
owned value. Later reference values are read after earlier callbacks, and auto
uses the language defaults active at that entry. No request-state borrow spans a
host callback or owner release. Invalid names stop iteration before later casts;
the request detection order changes only after the entire list validates. Native
raw iteration preserves existing Mixed cells so resource casts retain the original
resource owner. The ordinary snapshot reader keeps its normalized graph tags.
MbArraySourceV2 pairs the retained array with its original boxed-handle identity.
The eval adapter uses that original token to resolve reference metadata at each
entry read, then copies the current referenced value. Variable, nested-array,
instance-property, static-property, and invoker-slot references use explicit
ownership. Borrowed property values are retained; intermediate owners are released
through protected native callbacks after the eval context borrow ends. Pending
eval Throwables are published into native ownership before intermediate cleanup,
and native unwinding resumes only after the Rust adapter returns. The previously
ignored reference regression is active again.
Native global-to-array-element binding and eval methods reading an array global
created earlier in the same opaque fragment still have separate storage issues.
Native references to local indexed-array elements now normalize the array to
boxed Mixed slots and separate existing COW copies before binding. A captured
reference can then change type without writing a box pointer into a raw integer
element. This does not change the existing nonowning lifetime of element aliases.
Their reproducers and remaining lifetime/cleanup work are recorded in the mbstring
implementation ledger.
mb_encode_numericentity and mb_decode_numericentity share the same protected
array reader for conversion maps. Encoding resolution occurs before map-length
validation, and only complete groups of four elements are accepted. Map keys are
ignored. Integer conversion uses PHP arithmetic rules independently of caller
strictness: numeric prefixes may warn, finite float overflow wraps, and numeric
string overflow saturates. Ordered warnings are delivered before the next
referenced map element is read. The chosen encoding remains fixed across these
callbacks; substitution settings are read after map preparation. Empty input
still validates the encoding and every map element. Ordinary objects, arrays,
and resources are rejected without attempting Stringable conversion.
Production callbacks support native/eval Stringable methods and metadata, reference-value copying, binary diagnostic messages, and native float formatting. Current float formatting uses the runtime’s fixed precision. Preparation warnings use the existing native diagnostic sink; PHP error-handler dispatch remains pending. Operation diagnostics keep their existing result-buffer protocol. Protected release restores handler and GC suppression state after a PHP throw. Native array, hash, Mixed-cell, object, and callable-descriptor destruction now protects each potentially throwing child release. Each enclosing cleanup frame retains a pending flag and its incoming collector suppression state, finishes later children and its own storage, then propagates the latest exception. Nested destructor catches run with the outer pending exception suspended. Escaping exceptions consume that suspended owner into their previous chain; intersection checks avoid duplicate links and cycles. Shared previous access recognizes compact raw Throwable slots and ordinary subclass nullable boxes, including PDOException. Native and eval object destructors now preserve receivers retained by their callbacks, even when a callback throws. Persistent object kind-word bit 14 prevents a second destructor invocation; temporary refcount bit 31 protects only active teardown. The cycle collector recomputes roots after destructor callbacks while retaining its original candidate set. General function-local cleanup callbacks and copying values during active deep release still require separate work. These boundaries are not yet full cleanup parity.
Direct mbstring EIR calls now retain the physical file’s strict_types state,
including included functions and arrow functions. Generated callable wrappers
mark this state as inherited from the invoking caller. This parameter-coercion
profile is independent of --strict-php extension visibility and is visible in
textual EIR. Direct AOT adapters consume it when invoking the shared parameter
planner. Callable wrappers still apply their existing descriptor validation and
pass weak mode when this inherited profile has not been resolved.
The registry’s PreserveValues argument strategy keeps nullable/concrete source
types and disables generic scalar binding before mbstring. It reuses shared
named/spread planning, captures by-value sources before later argument expressions,
and leaves reference parameters attached to their original storage. Mixed cells
are cloned; concrete heap values are retained for the call. Typed EIR
exception.guard_owned calls register captured owners immediately, before later
argument expressions execute. Each 32-byte stack record contains the existing
activation prefix plus the owner, and selects either ordinary heap release or
callable-descriptor release from its concrete PHP type. Guards form parameter-order
cleanup groups while argument expressions retain PHP source evaluation order.
Successful invocations remove guards with exception.unguard_owned and use ordinary
EIR temporary cleanup. Failed argument evaluation and failed invocations both use
the common exception walker. Protected callbacks continue to later guards after a
destructor throws; scoped native deep release also finishes the interrupted
container’s remaining children and storage before propagating the exception.
Nested handlers save the current activation head so an inner catch preserves the
caller’s earlier captures. Mbstring effects include global writes because parameter
conversion can invoke PHP methods. Dynamic spread temporary ownership and other
call surfaces still require their separate ownership audits.
Positional indexed spreads use RuntimeArgumentLayout::IndexedArray on the same
profiled runtime target. Shared call planning supplies source order, and EIR builds
an owned array<mixed> containing every supplied value. Each element is copied
before evaluating the next source expression. Native heap arrays do not guarantee
pointer alignment, so the adapter copies their borrowed cell pointers into aligned
stack storage sized from the contract’s maximum arity. It passes the actual count
to the existing coordinator. Missing and excess argument counts therefore
produce catchable ArgumentCountError, including zero-parameter functions. Static
checking still evaluates every spread source and rejects non-array or undefined
inputs. exception.update_array_guard refreshes exceptional ownership after an
append relocates the array. Ordinary returns and both pre-entry/invocation failures
release the container and copied values, including when a native child destructor
throws during deep release. Dynamic associative spreads use the shared argument
planner too: string keys map to named contract parameters, values remain in PHP
source order, and the runtime applies the same duplicate, arity, and ownership
rules as direct arguments.
__rt_mbstring_stringable provides the protected host action for Stringable
preparation results. It reuses the existing native/eval string-context dispatcher,
installs a complete native exception-handler record, and returns a pending-throwable
status after an escaping PHP exception. Handler, activation-frame, and suppression
state are restored before returning to a Rust caller. Successful MbHostStringV1
results carry bytes, length, and a separate native owner. That owner is released
by the native GC helper, never by elephc_mbstring_release_v1.
__rt_mbstring_input describes an actual boxed value without coercing it. It
preserves scalar bits, binary strings, opaque array identities, nullable containers,
resources, and Closure identity. Native class tables supply borrowed class names
and Stringable capability; eval class lookup first resolves the object’s owning
context, falling back to the active caller context, and
returns a separately owned metadata string. The caller must retain argument values,
copy any preparation output that borrows this metadata, and release the metadata
owner before invoking PHP callbacks. This helper performs no array traversal or
Stringable execution. Production AOT and eval consume it through the same
coordinator.
The eval string-view ABI borrows bytes from an existing string cell, avoiding an unowned copy during metadata lookup. String-context exceptions transfer their raw Throwable owner before releasing the temporary boxed cell. Native constructor bridges borrow normalized argument cells from their private array, release key temporaries and replaced Throwable defaults, and release materialized parameter defaults after writeback. Mutable Mixed constructor reference slots own their staging cell, and an unchanged slot releases that owner after the call. Dynamic property assignment retains its borrowed cell for the property hash. Eval new-object expressions also release their directly allocated literal arguments after construction or failure. Ownership of other eval expression temporaries remains a separate, unfinished audit.
EncodingListBuilder resolves array-valued encoding lists incrementally: the host
converts one element, the engine resolves it using the currently active language,
and only then does the host advance. The builder retains the first auto expansion
and rejects finalization after a failed entry. This allows callbacks between
elements without retaining a Rust request-state borrow across PHP execution.
Callable routines
Source: src/codegen_support/runtime/callables/ (7 files including mod.rs)
These routines implement the runtime fallback path for is_callable() when the argument is not a compile-time literal or statically known callable value, plus the Closure::bind family helper. They consult generated metadata for builtins, user functions, public methods, public static methods, and __invoke objects.
Dynamic invocation builtins use generated callable descriptors rather than these boolean helpers. A descriptor is an eight-word record: callable kind, native entry pointer, PHP-visible name pointer, name length, signature-record pointer, environment-record pointer, invocation-record pointer, and optional uniform invoker pointer. Indirect calls keep the one-word callable ABI by loading the native entry from the descriptor, while descriptor-invoker paths call the generated (descriptor, boxed argument container) -> mixed adapter.
The signature side record stores visible, required, and regular parameter counts; variadic index; return type and return register count; declared-return flags; parameter names and types; defaults; by-reference flags; and declared-parameter flags. The environment record stores capture and hidden wrapper-parameter bindings. The invocation record stores the callable shape (string, callable array, closure, first-class callable, object __invoke, static method, instance method, builtin, extern, or user function) plus receiver, method, and auxiliary names where applicable.
call_user_func(), call_user_func_array(), direct string-variable calls, direct callable-array variable and literal calls, direct invokable-object calls, method first-class callable variable calls, and iterator_apply() compare runtime string names or descriptor-selected callable entries against generated cases. Runtime string callback names now materialize the matched descriptor and invoke its uniform invoker slot for user functions, extern wrappers, builtins, and public static methods, so signature defaults, named arguments, by-reference flags, variadics, and return boxing live behind the descriptor rather than in each string-dispatch callsite. iterator_apply() can also retain a branch-selected captured descriptor or runtime-selected callable-array descriptor and call that descriptor’s invoker directly for each loop iteration. array_map(), array_filter(), array_reduce(), array_walk(), usort(), uksort(), uasort(), and preg_replace_callback() retain descriptor-valued callable variables and callable parameters in descriptor callback environments, so by-value closure captures, method receivers, and late-static binding state are read from descriptor storage instead of current source locals. These callback runtimes also match runtime callable-array variables such as [$object, $method] or [$class, $method] against public method descriptor cases before invoking the shared descriptor callback wrapper. usort(), uksort(), and uasort() use descriptor comparator environments for branch-selected captured descriptors. CallbackFilterIterator and RecursiveCallbackFilterIterator store branch-selected captured descriptors and runtime-selected callable-array variable or literal descriptors in persistent callback environments, then call the same descriptor invoker from accept(). call_user_func() and direct callable-variable invocations build boxed indexed argument containers for descriptor-backed calls; named direct calls build associative hashes. Variable arguments in either shape can be encoded as internal reference-cell markers, including boxed markers inside named hash entries, so the generated invoker can either pass the original storage to by-reference parameters or dereference it for by-value parameters after reading the descriptor signature. call_user_func_array() clones the provided indexed array or hash, widens the clone to boxed Mixed, and passes one boxed container to the descriptor invoker. The invoker inspects the boxed container tag at runtime, dispatches to indexed-array or associative-hash argument materialization, unboxes boxed array/hash payloads for declared array parameters, and is keyed by callable signature rather than by caller array shape. Closure and first-class-callable descriptors with use (...), receiver, or late-static-binding context allocate runtime descriptor copies whose fixed header is followed by capture value slots; descriptor invokers reload those slots as hidden arguments, including by-reference captures. Method first-class callable variables use this descriptor path even when the original receiver variable still has static metadata, so reassignment of that source local cannot change the stored receiver. Callable variables and array elements whose descriptor was selected by an earlier runtime expression use the descriptor invoker when the local callsite no longer has static signature or capture metadata. Compile-time static-method callable arrays now materialize static-method descriptors for direct variable and literal calls, call_user_func() calls, and call_user_func_array() calls, including associative argument containers whose leftover string keys are forwarded into user-defined variadic method tails. Direct instance-method callable-array variable calls read the receiver from slot zero of the stored callable array, while direct instance-method callable-array literal calls evaluate the receiver before visible call arguments; both prepend it as descriptor argument zero and then let the descriptor signature normalize visible named/default/variadic/by-reference arguments. Direct invokable-object calls prepend the object as descriptor argument zero and use the object-invoke descriptor shape for the same named/default/variadic/by-reference handling, including non-local receiver expressions such as (new Runner())(...). Compile-time instance-method callable arrays and invokable objects use instance/object descriptor shapes for direct call_user_func() calls and for call_user_func_array() calls whose argument container is literal indexed, literal associative, dynamic indexed, dynamic associative, or runtime-opaque mixed/union; the receiver is prepended as a synthetic descriptor argument before the visible callback arguments. Receiver-bound runtime-opaque containers branch on their boxed payload tag, clone and widen the payload to Mixed entries, then build a receiver-prefixed indexed array or associative hash for the descriptor invoker.
Multiple runtime unpack sources in a dynamically resolved call are validated and copied in source
order, including named-key duplication and the rule against a numeric key after a named key within
one source. A non-empty positional source after an earlier named source needs segment-aware
parameter binding, which the boxed descriptor container does not encode yet. Elephc raises a
catchable Error with an explicit unsupported diagnostic for that shape instead of lowering it to
a different argument list.
Extern callback trampolines use the same descriptor invoker from a C-facing entry point. Each extern callable callsite has a generated descriptor slot and trampoline symbol; the trampoline reloads the current descriptor, boxes incoming scalar/pointer C callback arguments as a temporary indexed Mixed array, invokes the descriptor, casts the boxed result to int, float, bool, ptr, or void, and returns through the target C ABI.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_is_callable_string | Resolve a string as a builtin, active user function, or Class::method static-method callable | x1/x2 = string | x0 = bool |
__rt_is_callable_method_name | Check whether an object exposes a public method with the supplied name | object pointer + method string | x0 = bool |
__rt_is_callable_static_method_name | Check whether a class string exposes a public static method with the supplied name | class string + method string | x0 = bool |
__rt_is_callable_object | Check object callability through public __invoke metadata | object pointer | x0 = bool |
__rt_is_callable_array | Validate indexed callable arrays such as [$obj, "method"] or [ClassName::class, "method"] | array pointer | x0 = bool |
__rt_is_callable_assoc | Validate associative callable-array payloads produced through boxed or dynamic data paths | hash pointer | x0 = bool |
__rt_is_callable_mixed | Unbox a Mixed value and dispatch string, array, hash, or object callable checks | mixed pointer | x0 = bool |
__rt_is_callable_heap | Dispatch callable checks from a raw heap pointer by inspecting its heap-kind tag | heap pointer | x0 = bool |
__rt_callable_descriptor_release | Free a heap-backed callable descriptor copy plus the by-value capture slots appended after its static header; static .data descriptors are ignored | x0 = descriptor pointer | — |
__rt_closure_bind | Bind a closure whose first capture is $this to a new receiver for Closure::bind / Closure::bindTo / Closure::call: copy the 80-byte $this-only descriptor or the 96-byte class-scope descriptor whose second capture is the compiler-owned __elephc_called_class_id, overwrite the captured object, and incref it. Any user-capture shape aborts with a fatal diagnostic | x0 = source closure descriptor, x1 = new $this object | x0 = bound descriptor copy |
Array routines
Source: src/codegen_support/runtime/arrays/ (210 top-level Rust files plus 9 nested target/debug/test helpers)
Core allocation
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_heap_alloc | Free-list + bump allocator with a 16-byte [size:4][refcount:4][kind:8] header | x0 = size | x0 = pointer |
__rt_heap_grow_tail | Extend a live bump-tail allocation in place, updating its payload size and live/peak byte counters without counting a new allocation | x0 = pointer, x1 = requested payload bytes | unchanged pointer on success, null when the caller must fall back |
__rt_heap_free | Return block to free list (bump reset if last block) | x0 = pointer | — |
__rt_heap_free_safe | Free only if pointer is in heap range | x0 = pointer | — |
__rt_heap_debug_fail | Print a heap-debug fatal error and terminate immediately | x1 = msg ptr, x2 = msg len | — |
__rt_heap_debug_check_live | Reject incref / decref operations on already-freed heap blocks | x0 = pointer | — |
__rt_heap_debug_validate_free_list | Validate the ordered free list and small-bin chains before allocator mutations | — | — |
__rt_heap_debug_report | Print heap-debug exit summary with leak/high-water stats | — | — |
__rt_heap_kind | Return the uniform heap-kind tag for a heap-backed pointer | x0 = pointer | x0 = kind |
__rt_array_new | Create indexed array with header | x0 = capacity, x1 = elem_size | x0 = array ptr |
__rt_array_clone_shallow | Clone indexed array storage for copy-on-write splitting, retaining nested heap children as needed | x0 = array | x0 = new array |
__rt_array_to_mixed | Convert an indexed array’s live slots to boxed Mixed cells and stamp the array metadata as mixed | x0 = array, x1 = current value_type tag | x0 = unique array |
__rt_array_to_mixed_shared | Convert the payload already selected by a pending append, preserving callback-retained views of that write instead of separating again | x0 = selected array, x1 = current value_type tag | x0 = same array |
__rt_array_to_pointer_words | Consume one array owner, separate shared Mixed-slot storage, convert boxed nulls and integers to pointer words, and stamp pointer-element metadata | x0 = owned indexed array | x0 = owned pointer-word array |
__rt_hash_shift_boxed | Remove the insertion-order head, retain the removed value, and rebuild the hash with numeric keys renumbered and string keys preserved | first C argument = unique hash, second = removed-value output slot | owned rebuilt hash; removed box in output slot |
__rt_array_ensure_unique | Split a shared indexed array before mutation | x0 = array | x0 = unique array |
__rt_array_grow | Ensure uniqueness and double array capacity, extending a unique bump-tail allocation in place when possible or falling back to allocate-copy-free growth | x0 = array | x0 = grown array |
__rt_array_free_deep | Free array storage and release nested heap-backed elements | x0 = array | — |
__rt_array_union | Build PHP indexed-array union: left numeric keys win, only missing right suffix keys are appended | x0 = left array, x1 = right array | x0 = result array |
__rt_array_hash_union | Build PHP indexed+associative union by converting left indexes to integer hash keys before appending missing right entries | x0 = left array, x1 = right hash | x0 = result hash |
__rt_array_push_int | Append int to array (grows if needed) | x0 = array, x1 = value | x0 = array |
__rt_array_push_refcounted | incref borrowed heap payload, then append it as an 8-byte array element | x0 = array, x1 = heap ptr | x0 = array |
__rt_array_push_str | Persist string + append to array (grows if needed) | x0 = array, x1/x2 = str | x0 = array |
__rt_sort_int / __rt_rsort_int | In-place sort ascending or descending | x0 = array | — |
__rt_mixed_sort_require_scalars | Guard sort() / rsort() on a runtime-typed (Array(Mixed)) array: walks every element first and terminates with Fatal error: sorting Mixed arrays containing non-scalar values is not supported for a nested array, object, resource, or boxed callable, so an unsupported container is refused instead of ordered wrongly | x0 = array | — |
__rt_str_persist | Copy string from concat_buf to heap; a heap block stamped CONCAT_TEMP_HEAP_KIND is adopted in place instead of copied | x1/x2 = str | x1/x2 = heap str |
__rt_str_claim | Give a possibly transient string one owner without copying: retag a CONCAT_TEMP_HEAP_KIND block as heap kind 1 in place, return every other payload unchanged (lowering of EnsureOwned) | x1/x2 = str | x1/x2 = same str |
Common copy-producing array/hash routines now also have dedicated _refcounted siblings for nested heap-backed payloads. These variants retain borrowed values before pushing or inserting them into freshly allocated arrays/hash tables, covering array literals with spreads plus array_merge, array_chunk, array_slice, array_reverse, array_pad, array_splice, array_diff, array_intersect, array_fill, array_combine, and array_fill_keys. Filtering uses the storage-neutral boxed predicate runtime instead, preserving logical keys and PHP value types in an independently owned result.
| Refcounted sibling | What it does |
|---|---|
__rt_array_reverse_refcounted | Reverse an indexed array while retaining nested heap-backed elements |
__rt_array_merge_refcounted | Merge indexed arrays that carry nested heap-backed payloads |
__rt_array_slice_refcounted / __rt_array_splice_refcounted | Slice or splice while retaining nested heap-backed payloads |
__rt_array_fill_refcounted / __rt_array_fill_keys_refcounted | Build filled arrays/hashes from borrowed heap-backed values |
__rt_array_pad_refcounted | Pad an array with retained heap-backed values |
__rt_array_diff_refcounted / __rt_array_intersect_refcounted | Set-style comparisons that keep nested heap-backed values alive |
__rt_array_combine_refcounted | Combine key/value arrays into a hash while retaining heap-backed values |
__rt_array_chunk_refcounted | Split an array into retained heap-backed chunks |
__rt_array_merge_into_refcounted | Append one indexed array into another in-place while retaining nested heap-backed elements |
Hash table (for associative arrays)
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_hash_fnv1a | FNV-1a hash of string | x1/x2 = string | x0 = hash |
__rt_hash_normalize_key | Normalize PHP string array keys, converting integer-form numeric strings to integer keys | x1/x2 = string key | x1/x2 = normalized key |
__rt_hash_key_hash | Hash a normalized int/string array key | x1/x2 = normalized key | x0 = hash |
__rt_hash_key_eq | Compare normalized int/string array keys | x1/x2, x3/x4 = keys | x0 = equal flag |
__rt_hash_new | Create hash table | x0 = capacity, x1 = coarse value-type summary | x0 = hash ptr |
__rt_hash_clone_shallow | Clone hash storage for copy-on-write splitting, re-persisting keys and retaining nested heap values as needed | x0 = hash | x0 = new hash |
__rt_hash_ensure_unique | Split a hash shared by PHP value owners, excluding internal lifetime pins | x0 = hash | x0 = unique hash |
__rt_hash_pin | Retain a lifetime root without adding a PHP value owner | x0 = nonnull managed hash | x0 = same hash |
__rt_hash_unpin | Remove one pin and release its physical root through ordinary deep cleanup, which may invoke destructors | x0 = pinned hash | None |
__rt_hash_grow | Separate shared values, then replace entry storage at twice the capacity | x0 = hash | x0 = possibly separated hash |
__rt_hash_grow_owned | Rehash an internal construction array while preserving its header and aliases, without copy-on-write separation | x0 = hash | x0 = same hash |
__rt_hash_set | Insert/update (grows at 75% load) | x0=hash, x1/x2=normalized key, x3/x4=value, x5=value_tag | x0 = hash |
__rt_hash_set_shared | Insert into an already selected pending-write payload without another copy-on-write split | Same arguments as __rt_hash_set | x0 = hash |
__rt_hash_append | Append with PHP’s next automatic integer key (largest existing int key + 1, or 0), then delegate to __rt_hash_set | x0=hash, x3/x4=value, x5=value_tag | x0 = hash |
__rt_hash_insert_owned | Reinsert an already-owned key/value pair during hash growth | x0=hash, x1/x2=normalized key, x3/x4=value, x5=value_tag | x0 = hash |
__rt_hash_get | Look up value by key | x0=hash, x1/x2=normalized key | x0=found, x1=val_lo, x2=val_hi, x3=value_tag |
__rt_hash_unset | Remove one key for unset($hash[$key]): copy-on-write split, probe like __rt_hash_get, release the owned key/value payloads, tombstone the slot (so probe chains stay intact), and unlink it from the insertion-order chain; missing keys are a no-op | x0=hash, x1/x2=normalized key | x0 = (possibly cloned) hash |
__rt_hash_spread | Flatten a source hash into a destination hash with PHP [...$src] spread semantics: integer keys are re-sequenced from the destination’s next automatic key, string keys are preserved, later operands overwrite on collision, and each value is re-owned (strings persisted, refcounted payloads retained) | x0=dest hash, source hash | x0 = (possibly reallocated) dest hash |
__rt_hash_iter_next | Iterate to next entry in insertion order | x0=hash, x1=cursor | x0=next cursor, x1/x2=key, x3/x4=value, x5=value_tag |
__rt_hash_union | Build PHP associative-array union: left duplicate keys win, missing right entries append in insertion order | x0=left hash, x1=right hash | x0=result hash |
__rt_hash_array_union | Build PHP associative+indexed union by cloning the left hash and appending right indexes absent from the shared key space | x0=left hash, x1=right array | x0=result hash |
__rt_hash_count | Count occupied entries | x0=hash | x0=count |
__rt_hash_free_deep | Free a hash table plus owned keys and nested heap-backed values | x0=hash | — |
__rt_hash_to_mixed | Copy-on-write a hash, then widen each entry payload into a boxed Mixed cell so by-reference foreach can alias a stable pointer slot | x0=hash | x0 = same hash |
__rt_mixed_from_value | Box a tagged payload into a heap-allocated mixed cell | x0=value_tag, x1=value_lo, x2=value_hi | x0 = mixed cell |
__rt_mixed_write_stdout | Print a boxed mixed value by inspecting its inner tag | x0 = mixed cell | — |
__rt_hash_iter_next uses a small cursor protocol rather than a raw slot index: 0 starts from the hash header’s head, positive cursors encode slot_index + 1, -2 marks the post-tail state after yielding the final entry, and -1 means iteration is exhausted.
See Memory Model for the hash table memory layout.
Array manipulation
| Routine | What it does |
|---|---|
__rt_array_key_exists | Check if integer key is in bounds |
__rt_warn_undefined_array_key_int | Emit PHP’s Undefined array key warning for a missing integer key (warning-only; caller still supplies the null fallback) |
__rt_array_search | Linear search for value in indexed array |
__rt_array_reverse | Reverse element order |
__rt_array_sum / __rt_array_product | Sum/product of all elements |
__rt_array_shift / __rt_array_unshift | Remove/add at beginning |
__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_hash_slice | array_slice() over an ASSOCIATIVE source, including the hash payload of a boxed mixed or array value. $offset/$length count POSITIONS in insertion order, which a hash cannot answer by key, so the helper walks the source through __rt_hash_iter_next and counts. One helper serves both preserve_keys modes because they differ by a single per-entry decision: php-src renumbers INTEGER keys when the flag is false and leaves string keys alone either way. Ownership follows __rt_hash_clone_shallow (string keys and refcounted values are retained, string values re-persisted) because the window holds the same values the source does |
__rt_array_slice_str | The array_slice() copy for indexed string arrays, whose payload slots are 16-byte {pointer, length} pairs rather than the 8-byte slots the other slice helpers copy. The window arithmetic is the shared slice_bounds prologue, so offsets and lengths behave identically to every other variant; the copy DUPLICATES each pair through __rt_array_push_str (which persists via __rt_str_persist) because array_slice() leaves its argument untouched and an indexed string array owns its bytes exclusively — aliasing them would double free |
__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_to_hash_unique / __rt_hash_to_hash_unique | array_unique() for indexed and associative sources: dedupe by value through a second hash while preserving each survivor’s original key (an indexed source therefore returns a sparse hash) |
__rt_array_diff / __rt_array_intersect | Set difference/intersection by value |
__rt_array_diff_key / __rt_array_intersect_key | Set operations by key |
__rt_array_flip | Swap indexed integer values into associative-array keys |
__rt_array_flip_string | Swap indexed string values into associative-array keys, normalizing numeric-string keys |
__rt_array_combine | Combine key array + value array → AssocArray |
__rt_array_fill / __rt_array_fill_keys | Create filled arrays |
__rt_array_chunk / __rt_array_pad | Chunk/pad arrays |
__rt_array_column | Extract column from array of assoc arrays (int values) |
__rt_array_column_ref | Extract column of retained heap-backed values (arrays / hashes / objects) |
__rt_array_column_str | Extract column from array of assoc arrays (string values) |
__rt_array_column_mixed | Extract column values as boxed Mixed cells for heterogeneous input payloads |
__rt_range | Generate integer range array |
__rt_shuffle / __rt_array_rand | Randomize order / pick random |
__rt_random_u32 / __rt_random_uniform / __rt_random_u64 / __rt_random_uniform64 | Target-aware random primitives used by rand(), random_int(), shuffle(), and array_rand() |
__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. Both take PHP’s $flags word in the second argument register |
__rt_hash_key_sort_enter | Shared key-sort prologue: resolves PHP’s $flags into a small comparator selector once per call, the way php_get_key_compare_func does, and parks it clear of the direction and key/value mode bits |
__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: an allocation-free, stable bottom-up merge sort with O(n log n) comparisons 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_compare_entries | Reads and compares the heads of two merge runs with exact SORT_REGULAR key semantics, a flag-selected key comparison, or PHP’s general value comparison table |
__rt_key_compare_regular / __rt_key_compare_exact_decimal_integers / __rt_key_parse_i64_decimal | Key-comparison family behind the hash sorts: SORT_REGULAR key ordering with PHP’s exact numeric-string rules, including overflow-safe decimal-integer comparison |
__rt_key_compare_flagged / __rt_key_sort_int_bytes / __rt_key_sort_clip_nul | The key comparisons ksort()/krsort() select with $flags other than SORT_REGULAR. Every byte-comparing mode spells an integer key out as decimal digits into the comparator’s own frame first, then reuses __rt_strcmp, __rt_strcasecmp or __rt_strnatcmp; SORT_NUMERIC reads each key through __rt_str_to_number but compares two integer keys exactly rather than through a double; SORT_LOCALE_STRING clips both operands at the first NUL, which is strcoll in the C locale |
__rt_strnatcmp | PHP natural order (strnatcmp_ex) over two pointer/length strings, with an ASCII case-folding flag. A leaf with no calls: it runs once per comparison inside an O(n log n) sort |
__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_predicate_boxed | Search (array_find/array_any/array_all) or filter packed/hash arrays through value/key triples and PHP truthiness. Consumes a callback descriptor, borrows the source triple, and returns an owned Mixed result. Filter modes preserve keys; a null descriptor removes empty values. A retained source snapshot and resumable exception boundary protect all helper-owned values. |
__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 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
| Collector support routine | What it does |
|---|---|
__rt_gc_eval_object_children_count_all | Count all eval-owned object edges for the x86_64 incoming-edge cache, retaining the separate candidate-count and mark modes |
__rt_gc_weak_observers | Retire weak observers after resurrection/root recounting and before sweeping; callback graph changes trigger another mark pass and callback exceptions join the pending owner |
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_incref | Increment reference count (safe with null/non-heap pointers) | x0 = user pointer | — |
__rt_decref_any | Release any heap-backed value by inspecting the uniform heap-kind tag | x0 = pointer | — |
__rt_decref_array | Decrement refcount, deep-free indexed array if zero | x0 = array pointer | — |
__rt_decref_hash | Decrement refcount, free hash table if zero | x0 = hash pointer | — |
__rt_decref_mixed | Decrement refcount, deep-free mixed cell if zero | x0 = mixed pointer | — |
__rt_decref_object | Decrement refcount, free object if zero | x0 = object pointer | — |
__rt_gc_note_child_ref | Add one transient incoming edge to a heap child during cycle counting | x0 = child pointer | — |
__rt_gc_mark_reachable | Recursively mark array/hash/object blocks reachable from external roots | x0 = pointer | — |
__rt_gc_collect_cycles | Run the targeted cycle collector over heap-backed arrays/hashes/objects | — | — |
__rt_mixed_free_deep | Free a mixed cell and release any nested heap-backed payload; for tag-9 resources, dispatch the kind-specific destructor (kind 1 close, kind 2 __rt_hash_ctx_free, kind 3 __rt_pclose, kind 4 __rt_closedir) | x0 = mixed pointer | — |
__rt_object_free_deep | Free an object and release heap-backed properties using runtime/class metadata | x0 = object pointer | — |
Refcounts are stored as a 32-bit value in the uniform 16-byte heap header, at [user_ptr - 12]. Each heap allocation starts with refcount 1. When a reference is shared (e.g., assigned to another variable or passed to a function), __rt_incref bumps it. When the reference goes away, __rt_decref_any can dispatch through the uniform heap-kind tag to the concrete string/array/hash/object/mixed release path. 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 |
An object operand first enters the program-specific
__elephc_internal_object_compare function generated from the internal class
operator contracts. Its ordinary EIR static calls preserve hidden argument-count
and collector parameters. Result 3 requests the ordinary comparison table;
handled comparisons are equal only at 0, so unordered result 2 is false.
This also gives switch, loose in_array(), and recursive array/property
comparisons the decimal equality defined by BcMath\Number.
For unhandled values, 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/ (50 top-level Rust files plus 61 files in nested JSON, serialization, date, and parsing modules; 111 Rust files recursively)
__rt_build_argv — Build $argv array
File: system/build_argv.rs
At program start, the OS passes argc (argument count) in x0 and argv (pointer to C string pointers) in x1. This routine:
- Creates a new string array
- For each C string pointer in argv: measures the string length (scan for null byte), pushes ptr+len into the array
- Returns the array pointer
Core system routines
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_time | Get current Unix timestamp via gettimeofday syscall | — | x0 = seconds since epoch |
__rt_microtime | Get current time as float seconds via gettimeofday syscall | — | d0 = seconds.microseconds |
__rt_getenv | Get one environment variable via libc getenv(), copying the value out of the environment block so the caller can own it | x1/x2 = name string | Present: x1/x2 = owned value string (non-null even when empty); unset: a null pointer, which the caller boxes as PHP false |
__rt_getenv_all | Walk the live environ and build the whole environment as a string-keyed hash. Splits each entry at its first =, since a value may contain more | — | x0 = hash pointer |
__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_php_binary | Resolve the running executable path for PHP_BINARY, using procfs on Linux or dyld and realpath on Apple targets | None | owned PHP 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, then escape an active cdylib boundary or exit a standalone process 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. A cdylib has no process entry, so elephc_init() calls the same helper before the host enters exported PHP code; stack exhaustion then unwinds to the export boundary as ELEPHC_STATUS_RUNTIME_FAILURE instead of terminating the host.
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.
PCNTL routines
Files: system/pcntl.rs, system/pcntl_data.rs
The elephc-pcntl bridge owns the syscalls; these helpers are the adapters that
translate its stable C-ABI records into PHP values and drive PHP-visible signal
handlers from the compiled side. The bridge never sees a PHP value and the runtime
never declares a target’s struct rusage or siginfo_t layout — the record shapes
are elephc’s own (ElephcPcntlRUsage is a fixed 17-word block), which is what keeps
one bridge binary correct on macOS and Linux.
| Routine | What it does |
|---|---|
__rt_pcntl_rusage_array | Turn an ElephcPcntlRUsage record into PHP’s pcntl_wait() resource-usage array |
__rt_pcntl_siginfo_array | Turn a captured siginfo snapshot into the array a handler’s $siginfo parameter receives |
__rt_pcntl_dispatch_pending | Drain the pending-signal queue and invoke each registered PHP handler, replaying delivery counts preserved through a self-pipe overflow |
__rt_pcntl_async_dispatch_preserving | The same drain from an async-signals context, preserving the interrupted frame’s in-flight result |
__rt_pcntl_invoke_descriptor | Call one handler through its callable descriptor |
__rt_pcntl_abort_dispatch | Stop the drain when a handler throws, so the exception unwinds the interrupted frame instead of the dispatcher |
__rt_pcntl_release_handlers | Release the registered handler records after fork() or daemonization, so an inherited copy cannot fire in the child |
Compiled and eval’d handlers keep separate queues: a callable descriptor is private to the backend that made it, so the dispatcher routes a pending record only to the backend that registered its handler.
Exception routines
Source: src/codegen_support/runtime/exceptions.rs plus src/codegen_support/runtime/exceptions/ (23 top-level Rust files plus 2 nested cleanup helpers)
elephc lowers exceptions with a small runtime layer around the libc _setjmp / _longjmp pair on every supported target. These internal boundaries do not save or restore the signal mask, so ordinary protected releases avoid a signal-mask syscall and deliberate PHP signal-mask changes survive a throw. PCNTL dispatch restores its own temporary mask before exception unwinding resumes a handler outside that dispatch; exceptions caught inside a signal handler keep the current masked snapshot active. Codegen publishes the current exception object into _exc_value, pushes a handler record into _exc_handler_top, and then uses these helpers to unwind, match catch clauses, and resume control flow through catch / finally.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_exception_cleanup_frames | Walk the activation-record stack, run per-frame cleanup callbacks, and stop at the frame that should survive the catch | x0 = surviving activation record | — |
__rt_exception_guard_owned / __rt_exception_unguard_owned | Insert or unlink a stack-local argument owner while preserving parameter cleanup order | C ABI guard pointer and optional insertion anchor | Guard token or no value |
__rt_exception_release_owned / __rt_exception_release_callable | Clear captured ownership and run its typed release behind a protected PHP callback | C ABI guard pointer | Success or pending throwable status |
__rt_exception_matches | Check whether the active exception matches a catch target by class id or interface id | x0 = exception object, x1 = target id, x2 = 0 for class / 1 for interface | x0 = 1 if it matches, 0 otherwise |
__rt_instanceof_lookup | Resolve a dynamic class-string instanceof target through emitted case-insensitive class/interface metadata | x1/x2 = target string | x0 = found flag, x1 = target id, x2 = 0 class / 1 interface |
__rt_instanceof_invalid_target | Abort when a dynamic instanceof target is neither a string nor an object | — | does not return |
__rt_class_implements_interface | Test class metadata against an interface id for dynamic class-string checks without an object instance | x0 = class id, x1 = interface id | x0 = 1 if implemented, 0 otherwise |
__rt_throw_current | Unwind to the nearest active handler or print the fatal uncaught-exception message and exit | reads _exc_value, _exc_handler_top, _exc_call_frame_top | does not return normally |
__rt_rethrow_current | Re-enter the ordinary throw path with the currently active exception | none (uses global exception state) | does not return normally |
The fatal uncaught-exception path tail-jumps to __rt_report_uncaught_exception,
which reads the published _exc_value and writes
Fatal error: Uncaught <Class>: <message> in <file>:<line> to stdout before exiting
with status 255. The class name comes from _class_name_entries and the message
from payload offsets 8/16. The report and public getters share
__rt_throwable_location_borrowed, which resolves creation metadata and independent
ErrorException filename/line overrides without allocating. Getter strings are
persisted by their public adapter; the report borrows bytes from the published
receiver and saves pointer, length, and line across syscalls. An explicit zero
line or empty filename remains visible. A separate known-origin flag omits the
suffix for synthesized errors with no source location. An empty message drops
the ": " separator.
Ordinary Throwable objects, including ErrorException and subclasses with extra
properties, reserve a creation-line word after declared properties and the optional
dynamic-property hash. Scalar property writes cannot overwrite this metadata.
_class_throwable_line_offsets locates the word; _class_object_allocation_sizes
includes it, while _class_object_payload_sizes keeps the property and GC layout
unchanged. Fixed and dynamic EIR allocation stamp the allocating expression’s line
before the constructor runs. Location readers and uncaught reporting distinguish
ordinary metadata from the existing compact Throwable prefix. ErrorException
location getters use an explicit constructor override when present, including line zero. Explicit eval calls to
__construct reuse the constructor signature registration when ordinary method
metadata is absent, so named arguments and defaults use the same binding as new.
Codegen guards in codegen::lower_inst::exceptions have their OWN uncaught path: they write a fatal message baked at emit time and exit before the throwable is ever allocated, so they never reach this helper. They share UNCAUGHT_EXIT_STATUS with it so the status a script observes does not depend on which kind of exception escaped.
The runtime also resets the concat-buffer cursor before the final longjmp, so partially built string state from the throwing frame does not leak into the resumed catch/finally code.
Date/time routines
Files: system/date/, system/date_data.rs, system/mktime.rs, system/microtime.rs, system/hrtime.rs, system/getdate.rs, system/localtime.rs, system/checkdate.rs, system/date_default_timezone.rs, system/strtotime/
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_date / __rt_gmdate | Format a Unix timestamp using PHP date format characters (Y, m, d, H, i, s, l, F, T, e, O, P, …). __rt_date decomposes with libc localtime(), __rt_gmdate with gmtime() (UTC); both share the formatter and the static _day_names/_month_names tables. The T token reports "GMT" on the gmdate path | x1/x2 = format string, x0 = timestamp | x1/x2 = formatted string |
__rt_mktime / __rt_gmmktime | Create a Unix timestamp from date components (hour, minute, second, month, day, year). Populates a tm struct and calls libc mktime() (local) or timegm() (UTC) | x0-x5 = h, m, s, mon, day, year | x0 = Unix timestamp |
__rt_strtotime | Parse trimmed date/time strings through strategy emitters: ISO dates/datetimes (iso_date, which first zero-pads a one-digit month/day such as 2020-1-5 into _strtotime_iso_buf), M/D/Y slash dates (slash_date), textual dates like 15 January 2020 (textual_date), @<timestamp> epoch forms with an optional fraction and one ignored timezone token (epoch), first/last day of … and first/last <weekday> of … phrases (first_last_day), time-only forms, bare keywords (now, today, tomorrow, yesterday, midnight, noon), relative offsets (+1 day, 3 months ago, a/an <unit> article forms), and named weekdays with next / last / this. Successful paths populate a tm struct and call libc mktime(); malformed input returns the i64::MIN failure sentinel with a cleared success flag, and callers decide on the flag because i64::MIN is also a real timestamp (@-9223372036854775808) | x1/x2 = date string | x0 = Unix timestamp or sentinel, x1 = success flag (rax/rdx on x86_64) |
__rt_getdate / __rt_localtime | Decompose a timestamp into PHP’s getdate() / localtime() associative array via libc localtime(), defaulting the timezone to UTC through __rt_tz_init_utc on first use | x0 = timestamp | x0 = assoc array pointer |
__rt_checkdate | Validate a Gregorian month/day/year, leap-year aware | x0-x2 = month, day, year | x0 = 0/1 |
__rt_microtime / __rt_hrtime | Current wall-clock (gettimeofday) / monotonic (clock_gettime) time, as a float/string or [sec, nsec] array | flag | result |
__rt_date_default_timezone_get / __rt_date_default_timezone_set / __rt_tz_init_utc | Read / set the process default timezone (putenv("TZ=…") + tzset); __rt_tz_init_utc lazily defaults it to UTC like PHP | — / x1/x2 = id | — |
DateTimeZone introspection (getLocation(), getTransitions(), listAbbreviations()) is backed by the bundled elephc-tz workspace crate, which bakes PHP timelib’s IANA timezone tables into committed data files and exposes them through the elephc_tz_location / elephc_tz_transitions / elephc_tz_abbreviations ABI symbols. Like the elephc-tls and elephc-phar bridges it is linked only into programs that use it. The offset/DST resolution used by date()/gmdate() themselves is delegated to libc (localtime/gmtime/tzset).
JSON routines
Files: system/json_data.rs, system/json_depth.rs, system/json_throw_error.rs, system/json_last_error_msg.rs, system/json_validate/, system/json_decode.rs, system/json_decode_mixed/, system/json_encode_bool.rs, system/json_encode_null.rs, system/json_encode_float.rs, system/json_encode_str/, system/json_encode_array_int.rs, system/json_encode_array_str.rs, system/json_encode_array_dynamic.rs, system/json_encode_assoc.rs, system/json_encode_mixed.rs, system/json_encode_object.rs, system/json_integer_tree/, system/json_pretty.rs, plus objects/stdclass.rs for stdClass-specific JSON object encoding.
The structural decoder trims each borrowed token once. Numbers, literals and
containers consume that raw slice directly. Only a string token that has passed
validation enters __rt_json_decode for unescaping, avoiding another trim and
legacy-decoder call for every scalar or nested container. Both architecture
emitters preserve the same numeric overflow, error and ownership boundaries.
| Structural helper | What it does |
|---|---|
__rt_json_decode_mixed_value | Decode one recursive token while keeping the public call’s error and ownership boundary outside the recursive walk |
__rt_json_decode_syntax_error | Record malformed-token failure and return through the decoder’s cleanup path |
__rt_json_box_owned_container | Transfer a freshly built array, hash or object into its result Mixed cell without retaining a second construction owner |
__rt_json_integer_width | Calculate the exact decimal width of one signed integer, including its sign |
__rt_json_integer_tree_probe | Prove that an indexed-array tree contains only supported integer/array values and compute its complete encoded size |
__rt_json_integer_tree_try | Select the bounded integer-tree path, allocate its result once, or return an unsupported-shape result to ordinary dispatch |
__rt_json_integer_tree_write | Write a proved integer tree into the sized allocation without callbacks or further allocation |
The json_encode implementation uses type-aware dispatch — the codegen calls a different runtime routine depending on the compile-time type of the value being encoded:
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_json_encode_bool | Encode bool as "true" or "false" using static data labels | x0 = 0 or 1 | x1/x2 = JSON string |
__rt_json_encode_null | Encode null as "null" using a static data label | — | x1/x2 = JSON string |
__rt_json_encode_str | Encode a string with JSON escaping (quotes, backslashes, control chars) | x1/x2 = input string | x1/x2 = JSON string |
__rt_json_encode_array_int | Encode an integer array as a JSON array (e.g., [1,2,3]) | x0 = array ptr | x1/x2 = JSON string |
__rt_json_encode_array_str | Encode a string array as a JSON array with quoted elements | x0 = array ptr | x1/x2 = JSON string |
__rt_json_encode_array_dynamic | Encode an indexed array by inspecting its packed runtime value_type tag at runtime (int, string, float, bool, nested array/hash, mixed, or null fallback), caching active JSON flags in a callee-saved register during the walk | x0 = array ptr | x1/x2 = JSON string |
__rt_json_encode_assoc | Encode an associative array as a JSON object, while tracking PHP list-shape keys during the same hash walk and compacting to JSON array form when applicable | x0 = hash ptr | x1/x2 = JSON string |
__rt_json_encode_float | Encode finite floats and record JSON_ERROR_INF_OR_NAN for INF/NAN, honoring throw and partial-output flags | d0 = float | x1/x2 = JSON string |
__rt_json_encode_mixed | Encode a boxed mixed payload by unboxing its runtime tag and dispatching to the concrete JSON encoder | x0 = mixed ptr | x1/x2 = JSON string |
__rt_json_encode_object | Encode class objects by consulting per-class JSON descriptors; dispatches JsonSerializable::jsonSerialize() when present, otherwise walks public properties | x0 = object ptr | x1/x2 = JSON string |
__rt_json_encode_stdclass | Encode the dynamic-property hash backing stdClass, preserving {} for empty instances | x0 = stdClass hash ptr | x1/x2 = JSON string |
__rt_json_decode | String-only compatibility helper used by string decode paths; trims outer whitespace and unescapes quoted JSON strings including surrogate-aware \uXXXX sequences | x1/x2 = JSON string | x1/x2 = decoded string |
__rt_json_decode_mixed | Checked structural recursive decoder that returns boxed Mixed cells for null, bool, int, float, string, indexed arrays, associative arrays, and stdClass objects depending on _json_decode_assoc; records syntax/depth/UTF-8/UTF-16 errors plus source offsets and returns 0 on malformed input | x1/x2 = JSON string | x0 = Mixed* or 0 |
__rt_json_decode_mixed_array_real | Recursive array parser used by json_decode_mixed once the outer [ token is known | parser cursor + JSON bounds | boxed Mixed array |
__rt_json_integer_token | Probe a bounded JSON array token for a signed 64-bit integer without allocation or parser-state changes; leave the comma or closing bracket unconsumed | x1 = token pointer, x2 = remaining length | x0 = success; on success, x1 = integer and x2 = consumed bytes |
__rt_json_decode_mixed_object_real | Recursive object parser used by json_decode_mixed once the outer { token is known; returns assoc hash or stdClass payload based on decode mode | parser cursor + JSON bounds | boxed Mixed object/hash |
__rt_json_skip_ws | Shared RFC 8259 whitespace skipper used by json_decode_mixed and its recursive array/object parsers; advances a caller-owned cursor to the next token or caller-supplied limit | JSON slice pointer, exclusive limit, cursor | updated cursor |
__rt_json_validate | Standalone RFC 8259 validator used by json_validate(); scalar validator helpers are also reused by json_decode_mixed for strings and numbers | x1/x2 = JSON string | x0 = 1 valid / 0 invalid |
__rt_json_depth_enter / __rt_json_depth_exit | Maintain _json_active_depth and compare against _json_depth_limit for recursive encode/decode/validate walks | global JSON state | status / updated state |
__rt_json_note_error_candidate | Store an absolute candidate pointer, or one byte past the offending token, without reading the source | candidate pointer | updates _json_error_candidate_ptr |
__rt_json_set_error_location | If a nonempty decode or validate source is active, scan [source, min(candidate, source+len)) into one-based line/column data, then clear the source and candidate pointers. Called only from __rt_json_throw_error | global candidate and source slots | updates _json_error_location_active, _json_error_line, _json_error_column, then zeroes the source slots |
__rt_json_error_message | Build the current JSON error message, appending Elephc’s " near location line:column" suffix when decode or validate location state is active; PHP 8.2-8.5 base messages remain the comparison oracle | global JSON state | x1/x2 = message |
__rt_json_throw_error | Record a JSON error code and construct/throw JsonException when JSON_THROW_ON_ERROR is active, using the shared formatted JSON error message | x0 = JSON_ERROR_* code | may not return |
__rt_json_last_error_msg | Return the message string corresponding to _json_last_error through the _json_err_msg_table data table, including decode/validate location suffixes when active | global JSON state | x1/x2 = message |
__rt_json_pretty_push / __rt_json_pretty_pop / __rt_json_pretty_line / __rt_json_pretty_colon_space | Maintain _json_indent_depth and append PHP-style pretty-print whitespace while each container encoder emits bytes. These helpers are no-ops unless JSON_PRETTY_PRINT is active, avoiding a second buffer walk. | current JSON state, x11 write pointer for line/space helpers | updated formatting state / x11 write pointer |
Top-level indexed-array trees containing only integers, nested indexed arrays, and
boxed Mixed values holding either of those shapes can use the bounded integer-tree
encoder when no JSON flags are active. It first proves the complete tree and computes
its exact byte length without allocating, then allocates one result buffer and writes
the accepted tree without callbacks. The allocation carries CONCAT_TEMP_HEAP_KIND,
so the ordinary string persistence boundary adopts that owner once instead of copying
the bytes. Direct integer arrays with at most 3,120 slots keep the existing one-pass
scratch encoder. Flags, recursive encoder calls, references, hashes, strings, floats,
booleans, nulls, objects, unsupported layouts, and rejected depth or size proofs fall
back to the general encoder before observable JSON state or ownership changes.
Serialization routines
Files: system/serialize.rs, system/serialize/, system/serialization_hooks.rs, and system/unserialize/
These helpers back PHP’s serialize() / unserialize(). The serializer writes PHP’s exact wire format (N;, b:0;/b:1;, i:<int>;, d:<shortest-round-trip>;, s:<bytelen>:"<raw>";, a:<n>:{...}, and O:<len>:"<class>":<n>:{...}) directly into the concat buffer, reusing __rt_json_ftoa for shortest-round-trip float digits. Object serialization honors __sleep() / Serializable and reuses an object back-reference table so repeated instances emit r:/R: references. A class PHP refuses to serialize (the builtins php-src flags ZEND_ACC_NOT_SERIALIZABLE that elephc registers, listed in NOT_SERIALIZABLE_BUILTIN_CLASSES: the Reflection family, Generator, Fiber, the SplFileInfo family, Phar, PharData, and the PDO, curl, gd and XML handles, plus anything extending them) has its _class_serialize_ptrs entry pointed at __rt_throw_serialization_denied instead of a __serialize method. That helper throws a catchable Exception("Serialization of '<Class>' is not allowed"), so the denial holds wherever the serializer meets the object: typed, behind mixed, nested in an array, or on a subclass that declares its own __serialize(). Only elephc’s own declaration of a listed class counts (ClassInfo::is_internal, set for builtins registered without source and for classes from compiler-generated statements such as an injected prelude), so a namespaced user class such as Demo\WeakMap, a class named ReflectionType that elephc does not register, or a user class named like a prelude class the program does not load (CurlHandle or Pdo\Sqlite without the curl or PDO prelude), serializes normally. A Closure (runtime tag 10, including a Closure-typed property slot) throws the same Exception naming Closure through __rt_throw_serialization_denied_name. The default property walk leaves out a typed property that was never initialized (its slot’s high word holds the uninitialized sentinel) and counts only the properties it writes, as PHP does, so an unset Closure-typed property is simply omitted. The __sleep() path does the same through __rt_serialize_named_prop, which reports whether it wrote the named property; because that count is only known once every name was looked up, __rt_serialize_sleep_body writes the body first and __rt_concat_hoist_tail then moves the <count>:{ header in front of it inside the concat buffer. On the way back, __rt_obj_store_prop checks each parsed value’s boxed tag against the property’s _class_serpdiag_* row: a tag the declared type does not accept raises PHP’s catchable TypeError (Cannot assign <value> to property C::$p of type <T>) through __rt_unser_throw_property_type_error, and an accepted int is rewritten to a float in place only when the declared type includes float but not int (int|float keeps the integer). It then writes each restored slot’s high word (the string length, or zero), which clears the uninitialized marker of a typed property declared without a default.
Property metadata follows physical layout slots. A private parent property and a
child property with the same name keep distinct mangled keys, byte offsets, storage
tags and declaring-type diagnostics. The metadata resolver walks the ancestry for
a slot hidden by the child’s name map; the renderer’s __debugInfo() projection
does not change the serializer’s property list.
Option-validation TypeErrors use the entry that closes its pre-parser context; property TypeErrors leave that closure to the protected decoder boundary. Their common diagnostic body uses shared join labels: on Mach-O these are alternate entries, so branches from either global helper retain the common body under dead stripping instead of crossing into another helper’s local label.
unserialize() refuses the same classes from its allocation-free preflight. As soon as __rt_unser_validate_at has bounded an object’s class name, __rt_unser_refuse_class asks the allowed_classes policy (a blocked class still becomes __PHP_Incomplete_Class) and then matches the name case-insensitively against the per-program _unser_refused_classes table. That table lists the refused classes the program declares (subclasses included) and the refused builtins it does not declare at all (a Reflection class that declaration pruning removed, or Closure), but not a program’s own class that merely carries a builtin’s name. A match throws Unserialization of '<Class>' is not allowed, with the class spelled as declared, through __rt_throw_unserialization_denied_name. Because the preflight runs before the decoder, the refusal comes before any object, box or container is allocated and before any __wakeup()/__unserialize() hook of the payload runs (PHP defers those hooks until the whole payload decoded), so a caught refusal leaves nothing behind; it unwinds through the public entry boundary that closes the unserialize context.
The public __rt_serialize_call_value entry protects the reference counter and populated
object pointer/index pairs with a stack snapshot and guarded dispatch. Ordinary nested
operations restore that snapshot on success and on exceptions. The scoped adapters in
serialization_hooks.rs select shared registries for legacy Serializable callbacks
and isolated registries for modern hooks. Shared nested decodes promote their temporary
reference owners to the enclosing context before cleanup; calls without options inherit
its allowed-class policy. Native hook return metadata distinguishes strings, boxed values,
null, scalars, and other heap owners, so invalid serialize results throw after cleanup and
ignored unserialize results are released. EIR declares serialize results independent and
unserialize results fresh, allowing throwing builtin operand guards to retain and release
temporary inputs correctly.
The unserializer keeps the fixed runtime-emission order in unserialize/mod.rs. Shared diagnostics and per-call context lifecycle are separated from target-specific allowed-class policy parsing, allocation-free validation, recursive decoding, and object-storage/key helpers. The two decoder files are cohesive architecture leaves; every surrounding orchestration or support module remains below the repository’s 500-line warning threshold.
Before hydration writes a property slot, __rt_unser_release_prev_prop releases the value already there: the class default, or a value written by an earlier duplicate property key. The release is skipped when the metadata word is the uninitialized typed-property sentinel. Strings, arrays/hashes/objects, and reference cells release through the uniform dispatcher, and callable descriptors through their capture-aware helper. Mixed slots are left to the store, which reuses a live box in place so an earlier R: slot follows the replacement, while in-band null and uninitialized sentinels are not treated as boxes. Without this step the default Mixed, string, array, and object values of every hydrated property leaked when the parsed value replaced them.
After a parsed value is written into a declared property slot, __rt_unser_finish_stored_prop releases the parser box or transfers/retains the payload the slot now owns. Scalar slots copy the payload inline and free the box, string slots transfer or retain the persisted string, and container slots keep the registry box alive until the decoder context completes. An unknown serialized property key defers its parsed value to the same context instead of stranding it.
Every parsed hash entry is inserted through __rt_unser_store_entry instead of calling __rt_hash_set directly. When a duplicate key replaces an entry, the helper reuses that entry’s existing Mixed box, retains the replaced payload through the parser context, and moves the replacement into the same box. Both the earlier R:/r: slot and the duplicate value’s own slot therefore keep addressing live storage, and the replaced value’s destructor runs only after decoding completes. Scalar replacements discard their temporary box; array, hash, and object replacements keep it as a nested wrapper at their own reference slot.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_serialize_call_value | Guard a public call and preserve shared or isolated reference state across hooks | value tag + payload | string slice |
__rt_serialize_guarded_dispatch | Dispatch serialization inside a protected boundary so reference snapshots are restored before propagating hook exceptions | saved call state | serialization result or pending exception |
__rt_serialize_serializable | Invoke a legacy Serializable serializer and write its opaque C: envelope when a modern hook does not supersede it | object and serializer metadata | serialized bytes |
__rt_serialize_value | Tag-dispatching serializer for a raw runtime value, appending its wire form to the concat buffer | value tag + payload | x1/x2 = string slice |
__rt_serialize_mixed | Unbox a boxed Mixed cell (null pointer → N;), then serialize it | x0 = mixed cell | x1/x2 = string slice |
__rt_serialize_indexed_array / __rt_serialize_hash | Serialize indexed arrays and hashes as a:<n>:{...} | array/hash pointer | x1/x2 = string slice |
__rt_serialize_object / __rt_serialize_named_prop / __rt_serialize_obj_ref | Serialize objects (O:/C:), emit one named property entry, and resolve back-references | object pointer | x1/x2 = string slice |
__rt_unserialize_begin / __rt_unserialize_mixed / __rt_unserialize_object | Parse a serialized string back into boxed Mixed cells, including nested arrays/hashes and objects | x1/x2 = serialized string | x0 = Mixed* (0 on malformed input) |
__rt_unserialize_inherit_policy | Inherit the enclosing allowed-class policy for a nested legacy decode without explicit options | current parser context | inherited policy |
__rt_unserialize_promote_legacy_data | Move temporary reference owners from a shared nested decode into the enclosing context before cleanup | nested parser context | enclosing ownership list |
__rt_unser_store_entry | Insert a parsed box and reuse an existing box on a duplicate key, deferring the replaced payload | x0 hash, x1/x2 key, x3 box | x0 = possibly reallocated hash |
__rt_unser_reuse_box | Replace a Mixed box in place while keeping serialized reference slots valid and deferring replaced owners to the parser context | x0 = destination box, x1 = parsed replacement box | x0 = destination box |
__rt_unser_finish_stored_prop | Release or retain a parsed box after its property slot took the copied payload | x0 box | none |
__rt_unser_release_prev_prop | Release the default or earlier value a property hydration overwrites | x0 slot, x1 tag | none |
__rt_unser_release_payload | Release only the payload a replaced box owned, leaving the box itself for reuse | x0 boxed tag, x1 payload | none |
__rt_concat_hoist_tail | Move the bytes appended since a tail offset (at most 32, such as a <count>:{ header) in front of the body written before them | x0 = body start, x1 = tail start (buffer offsets) | concat buffer rearranged, write offset unchanged |
__rt_unser_refuse_class | Refuse a class PHP never unserializes, from the preflight: ask the allowed_classes policy, then scan _unser_refused_classes | x1/x2 = wire class name | returns when allowed; otherwise throws |
Regex routines
Files: system/preg_strip.rs, system/pcre_to_posix.rs,
system/mb_ereg_match.rs, system/preg_match.rs,
system/preg_match_all.rs, system/preg_match_all_capture.rs,
system/preg_replace.rs, system/preg_replace_callback.rs,
system/preg_split.rs; the embedded shim
source is src/native_deps/recipes/pcre2_shim.c.
Generated runtime assembly calls only the versioned
elephc_pcre2_v1_compile, elephc_pcre2_v1_exec, and
elephc_pcre2_v1_free ABI. The Elephc-owned C shim uses PCRE2’s POSIX wrapper
internally, but no regex_t, regmatch_t, regoff_t, or other PCRE2-owned
layout crosses the boundary. compile returns an opaque handle and slot count;
exec writes fixed 16-byte [start, end] signed-64-bit pairs, initializing
unmatched and surplus slots to [-1, -1].
__rt_preg_strip strips PHP-style delimiters and maps supported modifiers (i,
m, s, u, U) to shim compile flags. __rt_pcre_to_posix keeps its
historic symbol name for compatibility but only materializes a null-terminated
pattern. __rt_mb_ereg_match backs mb_ereg_match() through the same shim. The
whole regex family is emitted only when the program’s RuntimeFeatures request
it. A final-link regex program resolves the managed pcre2 package and links
exact verified archives in shim, POSIX, then 8-bit order. Dynamic eval does so
only when static regex detection or --with-regex requests the same feature.
There is no production system-PCRE2 fallback; compilation itself never installs
a package.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_preg_match | Test if a regex matches the subject string. Compiles the pattern, executes once, frees | pattern + subject strings | x0 = 1 (match) or 0 (no match) |
__rt_preg_match_capture | Test once and materialize PHP’s optional $matches array from the shim-reported slot count and fixed offset pairs, omitting trailing unmatched captures while keeping interior unmatched captures as empty strings | pattern + subject strings | match flag plus matches array pointer |
__rt_preg_match_all | Count all non-overlapping matches by repeatedly executing the regex with advancing offsets | pattern + subject strings | x0 = match count |
__rt_preg_match_all_capture | Fill preg_match_all()’s by-reference $matches for numbered groups. PREG_PATTERN_ORDER uses $matches[group][match], PREG_SET_ORDER uses $matches[match][group], PREG_OFFSET_CAPTURE boxes [value, offset], and PREG_UNMATCHED_AS_NULL uses null (offset -1 with offset capture). Unsupported extra flag bits, or both order bits at once, return count 0 and [] | pattern + subject strings + flags | x0/rax = match count, x1/rdx = matches array |
__rt_preg_replace | Replace all regex matches with a replacement string. Builds the result incrementally in the concat buffer and expands $0..$99 / \0..\99 from the PCRE2 capture vector | pattern + replacement + subject | x1/x2 = result string |
__rt_preg_replace_callback | Replace all regex matches using shim-sized fixed-pair capture storage, building an indexed $matches string array, invoking the callback, and appending its string result while preserving concat-buffer state across callback prologues | pattern + callback + subject | x1/x2 = result string |
__rt_preg_split | Split the subject string at regex match boundaries using shim-sized fixed-pair capture storage. Applies limit, no-empty, delimiter-capture, and offset-capture flags; dynamic flags return boxed Mixed slots to preserve layout | pattern + subject strings, limit, flags | x0 = array pointer |
I/O routines
Source: src/codegen_support/runtime/io/ (126 top-level files plus 1 nested response-metadata helper)
These routines handle file and filesystem operations through target-aware libc/syscall helpers. PHP strings (pointer + length) must be converted to null-terminated C strings before passing to C or OS APIs — __rt_cstr handles the primary buffer and also emits __rt_cstr2 for routines that need a second simultaneous C string.
The first table covers the file/filesystem core; the subsections after it cover the stream/networking surface emitted from the same directory: stream contexts and metadata, stream filters and user-defined stream wrappers, TCP/Unix/IPv6 sockets, TLS/SSL helpers, FTP/HTTP transfer helpers, hostname/service resolution, phar archives, and var_dump.
| Routine | What it does |
|---|---|
__rt_cstr | Convert PHP string (ptr+len) to null-terminated C string |
__rt_fopen | Open file via target-aware open() handling, or return -1 after emitting a suppressible warning for open failures and invalid modes |
__rt_fgets | Read line from file descriptor |
__rt_fgetc | Read a single byte from a file descriptor (tail-calls __rt_fread with length 1) |
__rt_feof | Check end-of-file flag for a file descriptor |
__rt_fread | Read N bytes from file descriptor |
__rt_readfile | Open a path, stream contents to stdout, and return copied byte count, -1 on read failure, or a false sentinel on open failure |
__rt_fpassthru | Stream the remaining bytes from an existing descriptor to stdout and return copied byte count or -1 on read failure |
__rt_flock | Call libc flock(), translating PHP’s LOCK_UN constant and exposing would-block state for the optional output parameter |
__rt_tmpfile | Create an anonymous temporary file descriptor through mkstemp() plus immediate unlink |
__rt_file_get_contents | Read entire file into string, or return a null pointer after emitting a suppressible warning on failure |
__rt_file_put_contents | Write string to file (create/truncate) |
__rt_file | Read file into array of lines |
__rt_file_exists / __rt_is_file / __rt_is_dir | Existence and path-type checks backed by stat() |
__rt_is_readable / __rt_is_writable | Access checks backed by access() on real paths |
__rt_stat_mode_access | The wrapper-aware permission predicate behind is_readable() / is_writable() / is_executable() on userspace-wrapper paths: selects exactly one owner/group/world triad from the url_stat()-reported uid/gid against the process identity (getuid/getgid/getgroups) |
__rt_filesize / __rt_filemtime / __rt_fileatime / __rt_filectime / __rt_fileperms / __rt_fileowner / __rt_filegroup / __rt_fileinode | Stat scalar metadata. Each returns a payload plus a success flag (x1/rdx) so codegen can box PHP false without confusing legitimate zero values — a size of 0 for an empty file, or a timestamp of 0. |
__rt_filetype / __rt_is_executable / __rt_is_link | File type and permission predicates; filetype() uses lstat() so symlinks report "link" and missing paths box as false. |
__rt_stat_array / __rt_lstat_array / __rt_fstat_array | Build PHP-compatible stat arrays with numeric and string keys, returning a null pointer for codegen to box as false on failure |
__rt_unlink / __rt_mkdir / __rt_rmdir / __rt_chdir | Filesystem path operations via libc/syscalls |
__rt_rename / __rt_copy | Two-path filesystem helpers using dual C-string scratch buffers |
__rt_symlink / __rt_link | Create symbolic or hard links through libc |
__rt_readlink | Read a symbolic-link target into a heap-backed string, with null output for PHP false on failure |
__rt_linkinfo | Return lstat() device metadata for a link path, or PHP’s -1 failure sentinel |
__rt_getcwd | Get current working directory |
__rt_scandir | List directory contents into array |
__rt_glob | Pattern-match filenames |
__rt_tempnam | Create temporary filename |
__rt_fgetcsv | Parse CSV line from file |
__rt_fputcsv | Write CSV line to file |
__rt_basename / __rt_dirname / __rt_dirname_levels | Compute path components for basename() / dirname() including repeated parent traversal |
__rt_fnmatch | Match shell-style path globs with PHP/libc-compatible flag bits for the selected target |
__rt_realpath | Canonicalize an existing path, returning a null pointer on failure so codegen can box PHP false |
__rt_pathinfo_str / __rt_pathinfo_array | Return one pathinfo() component for component flags, or build the associative-array PATHINFO_ALL shape |
__rt_parse_url | Scan PHP-compatible URL components, persist strings, and box either a selected component or a Mixed-valued associative hash |
__rt_chmod / __rt_chown / __rt_lchown / name-resolving variants | File ownership and mode modification helpers, including symlink-aware ownership updates |
__rt_lookup_passwd_uid / __rt_lookup_group_gid | Resolve local user/group names by scanning /etc/passwd and /etc/group without calling NSS, so static Linux binaries do not require glibc NSS modules at runtime |
__rt_umask / __rt_ftruncate | Process umask and file truncation helpers |
__rt_fsync / __rt_fflush / __rt_fdatasync | File descriptor flush helpers; fflush() maps to fsync() because elephc has no userspace stdio buffer |
__rt_touch | Create missing files and update access/modification timestamps |
Stream and socket routines
| Routine | What it does |
|---|---|
__rt_stream_socket_client / __rt_stream_socket_client_v6 | Open TCP client connections (IPv4/IPv6) with timeout handling |
__rt_stream_socket_server / __rt_stream_socket_server_v6 | Bind and listen on TCP server sockets (IPv4/IPv6) |
__rt_unix_socket_client / __rt_unix_socket_server | Unix-domain socket client/server endpoints |
__rt_stream_socket_accept | Accept a pending connection with optional timeout |
__rt_stream_socket_pair | Create a connected socket pair |
__rt_stream_socket_recvfrom / __rt_stream_socket_sendto | Datagram receive/send with peer-address formatting |
__rt_stream_socket_get_name | Local or remote endpoint name for a socket |
__rt_stream_socket_shutdown | Half/full shutdown of a connected socket |
__rt_socket_backlog / __rt_apply_socket_bindto / __rt_apply_socket_client_opts / __rt_apply_socket_server_opts | Socket-option plumbing for context-driven behavior |
__rt_stream_select | stream_select() over descriptor arrays via poll()/select() |
__rt_stream_set_blocking / __rt_stream_set_timeout | Per-descriptor blocking mode and read timeout |
__rt_stream_get_contents / __rt_stream_get_contents_bounded / __rt_stream_get_line | Bulk and line-delimited stream reads |
__rt_stream_copy_to_stream | Copy bytes between two descriptors |
__rt_stream_get_meta_data | Build the stream_get_meta_data() associative array |
__rt_stream_context_set_option_4 | Store context options consumed by the open/transfer helpers |
__rt_stream_isatty | TTY detection for descriptors |
__rt_data_stream | data:// stream payload decoding |
__rt_get_ssl_peer_name | Peer-certificate name lookup for TLS streams |
Networking and transfer routines
| Routine | What it does |
|---|---|
__rt_http_open / __rt_https_open | Open http:// / https:// streams (TLS via the elephc-tls bridge) |
__rt_http_build_request | Assemble the HTTP request from context options (method, headers, body, request_fulluri) |
__rt_http_fire_notification | Invoke the stream-notification callback during transfers |
__rt_ftp_open / __rt_ftp_send_recv / __rt_ftp_parse_pasv | ftp:// stream support (control dialog, passive-mode parsing) |
__rt_resolve_host / __rt_resolve_host_v6 | Hostname resolution to IPv4/IPv6 addresses |
__rt_gethostbyname / __rt_gethostbyaddr / __rt_gethostname | Host lookup builtins |
__rt_getprotobyname / __rt_getprotobynumber / __rt_protoent_load | Protocol-database lookups backed by /etc/protocols |
__rt_getservbyname / __rt_getservbyport | Service-database lookups backed by /etc/services |
User stream wrappers and filters
Userspace streamWrapper classes registered with stream_wrapper_register() dispatch through a vtable of __rt_user_wrapper_* routines (fopen/fread/fwrite/fclose/feof/fseek/ftell/fflush/fstat/ftruncate/flock/set_option/stream_cast, the dir_* family, path_op, and rename), each bridging the synthetic descriptor back to PHP method calls on the wrapper instance. Stream filters use __rt_stream_filter_register, __rt_apply_stream_filter / __rt_apply_user_stream_filter, __rt_stream_filter_attach_user, __rt_resolve_user_filter_id, __rt_user_filter_brigade_invoke, and __rt_user_filter_release_fd to run built-in (zlib.*, bzip2.*, convert.iconv.*, string.*) and user-defined filter chains over stream reads and writes.
A convert.iconv.* write filter keeps its libc iconv_t open across writes. Apple’s iconv (Citrus-based since macOS 14) stores the //TRANSLIT and //IGNORE options on the converter it shares between every descriptor of one charset pair, and any iconv_open() for that pair, the iconv bridge’s included, rewrites them. On Apple targets the attach therefore records the filter’s own option bits in _iconv_write_options[fd], and the write helper restores them with iconvctl() before each conversion, the same protocol the elephc-iconv bridge follows. The read transform needs no restore: it opens its descriptor immediately before its only conversion and closes it right after. glibc keeps the options per descriptor, so Linux code has neither step.
Phar archive routines
| Routine | What it does |
|---|---|
__rt_fopen_maybe_phar / __rt_file_get_contents_maybe_phar | Route dynamic phar:// read paths to archive entry reads and write-mode fopen() paths to PHAR write streams, falling through to plain file I/O otherwise |
__rt_phar_read_entry | Locate and read one entry from a PHAR URL through the elephc-phar bridge, which handles native PHAR, tar, and ZIP containers and authenticates OpenSSL signatures with <archive>.pubkey; missing/invalid keys and a missing bridge fail closed rather than entering the legacy unauthenticated assembly parser |
__rt_phar_write_open / __rt_phar_write_open_url / __rt_phar_write_append / __rt_phar_write_finalize / __rt_file_put_contents_maybe_phar | Buffer phar:// write entries in bridge-owned descriptor slots, then finalize each through the elephc-phar bridge so native PHAR, tar, and ZIP archives preserve existing entries; runtime-built file_put_contents() and fopen() write URLs call a bridge variant that splits the full phar:// URL; the assembly fallback still emits a single-entry SHA1-signed native archive |
var_dump output routines
var_dump() lowering calls a family of __rt_var_dump_array_* routines (int, float, str, bool, mixed) that walk array payloads, __rt_var_dump_hash for associative arrays, and a set of __rt_var_dump_emit_* helpers that print one typed line (int(...), float(...), bool(...), string headers, indexed keys) with the PHP-compatible indentation. The walkers do not issue raw write syscalls: they call __rt_vd_write, a register-preserving shim that routes the bytes through __rt_stdout_write (so output buffering, print_r return-mode capture, and --web capture all see var_dump output) while saving and restoring every register — including the float walker’s pending d0 — that the walkers expect a raw syscall to leave untouched.
The stdout funnel and --web helpers
Every terminal stdout write in a compiled program travels through one indirection, __rt_stdout_write (io/stdout_write.rs). Its branch order is part of the output contract:
- print_r return-mode capture — while
_print_r_modeis set, append the bytes to_print_r_bufvia__rt_pr_appendinstead of writing - user output-handler guard — while
_ob_in_handleris set, discard the bytes entirely (PHP discards output produced inside anob_start()handler) - output-buffer capture — while the
ob_*stack is non-empty (_ob_level> 0), append the bytes to the top output buffer via__rt_ob_append --webcapture — in--webbuilds only, a non-zeroelephc_web_captureflag routes the bytes toelephc_web_write; default worker isolation buffers them, while pool/request isolation frames them onto the handler response stream- plain
write(1, ptr, len)syscall — the universal fallback
The --web capture branch is emitted only when compiling with --web, so ordinary binaries never reference the bridge symbols. A few related helpers are always emitted so their EIR calls resolve on every build, with bodies that differ under --web: __rt_php_input (reads the request body for file_get_contents('php://input'), false otherwise) and __rt_http_response_code / __rt_header (call the bridge setters under --web, no-ops otherwise). PHP session support (session_*) is not part of this runtime: it lives in the --web PHP prelude (src/web_prelude.rs) as elephc_web_session_* extern functions provided by the web bridge.
print_r rendering and capture routines
print_r() uses walker helpers that mirror the var_dump family — __rt_print_r_spaces, __rt_print_r_open / __rt_print_r_close, __rt_print_r_int_key / __rt_print_r_str_key, __rt_print_r_value, __rt_print_r_indexed, and __rt_print_r_hash (whose entry lines come from __rt_print_r_hash_entries) — all writing through __rt_pr_write. Object bodies (__rt_var_dump_object, __rt_print_r_object, and the __rt_obj_prop_* accessors behind var_export) list the declared properties from the per-class descriptor, then the instance’s dynamic-property hash in insertion order, located by __rt_obj_dump_dyn_props (__rt_obj_dyn_prop_at for indexed access). The _class_dump_dyn_prop_flags table gates that tail off for classes whose __debugInfo() folded into a projection. Return mode (print_r($value, true)) is backed by three capture helpers in io/print_r_buffer.rs:
| Routine | What it does |
|---|---|
__rt_pr_append | Append bytes to the 64KB _print_r_buf at _print_r_off, clamping to the remaining capacity so oversized captures truncate instead of overflowing |
__rt_pr_write | Walker-facing write wrapper: branch on _print_r_mode between the stdout path and __rt_pr_append |
__rt_pr_finish | Persist the captured bytes to an owned heap string via __rt_str_persist and reset the capture state |
Output buffering (ob_*) routines
Added with 0.26.2, the ob_* builtins are backed by a runtime buffer stack (io/ob_buffer.rs, io/ob_handler.rs, io/ob_status.rs). State lives in fixed data: _ob_level is the nesting depth, and 64-slot parallel arrays (_ob_ptrs / _ob_lens / _ob_caps plus _ob_handler_stubs / _ob_handler_envs / _ob_name_ptrs / _ob_name_lens / _ob_chunk_sizes / _ob_flags / _ob_started) hold each level’s heap-allocated buffer and handler metadata. Buffer capacity is PHP-shaped: 16384 bytes by default, or the page-aligned chunk size + 1 when a chunk size is given, growing by doubling. These helpers are always emitted because __rt_stdout_write, __rt_pr_write, and the process-exit paths reference them unconditionally.
| Routine | What it does |
|---|---|
__rt_ob_start_ex / __rt_ob_start | Push a new output buffer with handler stub + env word, chunk size, flags, and persisted display name; __rt_ob_start is the default-handler compatibility wrapper. Calling ob_start() inside a running handler is a fatal error |
__rt_ob_append | Append bytes to the top buffer, growing it as needed; reaching a non-zero chunk-size threshold triggers an auto-flush with the WRITE handler phase |
__rt_ob_contents | Return a persisted copy of the top buffer contents |
__rt_ob_length / __rt_ob_level | Integer queries: top buffer’s used byte count (-1 when inactive) / nesting depth |
__rt_ob_process_and_write | Shared flush/clean core for one slot: run the handler phase, then emit or discard the buffered bytes |
__rt_ob_pop_free | Pop the top buffer, releasing its storage and handler resources |
__rt_ob_clean / __rt_ob_end_clean / __rt_ob_flush / __rt_ob_end_flush | The four flags-gated bool-returning mutations, with PHP’s per-operation gating flags, handler phases, and notice texts |
__rt_ob_get_clean_pop / __rt_ob_get_flush_pop | Composite helpers behind ob_get_clean() (silent on no buffer) and ob_get_flush() (PHP notice on no buffer) |
__rt_ob_flush_all | Drain every still-active buffer at process exit, top-down with the FINAL handler phase, guarded by _ob_flushing against handler-triggered re-entry; gating flags are ignored because PHP force-flushes at shutdown |
__rt_ob_apply_handler | User-handler dispatch core: compute the handler phase (ORing in START on the first run), set _ob_in_handler around the call so handler output is discarded |
__rt_ob_result_to_bytes | Map a handler’s Mixed result to the replacement bytes: false passes the original through, anything else is cast to string and persisted |
__rt_ob_invoke_descriptor | Invoke an AOT callable-descriptor handler through its uniform (descriptor, mixed-arg-array) invoker |
__rt_ob_eval_trampoline | Invoke an eval-registered handler through the installed magician hook (_elephc_eval_ob_handler_fn) |
__rt_ob_notice_named | Write the PHP-parity Failed to ... buffer of NAME (LEVEL) notice |
__rt_ob_get_status / __rt_ob_status_entry / __rt_ob_list_handlers | Build the ob_get_status() status hash (simple and full modes) and the ob_list_handlers() name array |
Handler stubs use a uniform ABI — stub(env, buf_ptr, buf_len, phase) returns a replaced-flag plus the owned replacement string — where AOT handlers pass a retained callable-descriptor pointer as env and eval handlers pass a magician registry id. All ob_* notices and warnings are ordinary output routed through __rt_stdout_write, so active parent buffers capture them exactly like PHP.
Pointer routines
Source: src/codegen_support/runtime/pointers/ (7 files including mod.rs)
These helpers support the compiler-specific pointer builtins.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_ptoa | Format a pointer value as a lowercase hexadecimal string with 0x prefix, reserved through __rt_concat_reserve (concat scratch while it fits, a transient heap block once the scratch is full) and published through __rt_concat_publish; shared by echo and every pointer-to-string conversion | x0 = pointer/address | x1/x2 = formatted string |
__rt_ptr_check_nonnull | Abort with Fatal error: null pointer dereference if the pointer is null | x0 = pointer/address | x0 unchanged on success |
__rt_str_to_cstr | Copy an elephc string to temporary null-terminated heap storage for a native call | x1/x2 = string | x0 = C string pointer |
__rt_cstr_to_str | Copy a borrowed null-terminated C string back into an owned elephc string | x0 = C string pointer | x1/x2 = elephc string |
__rt_ptr_read_string | Copy a fixed-length byte range from a raw pointer into an owned elephc string | x0 = pointer, x1 = length | x1/x2 = elephc string |
__rt_ptr_write_string | Copy an elephc string’s bytes into the memory addressed by a raw pointer | x0 = pointer, x1/x2 = string | — |
zval bridge routines
Source: src/codegen_support/runtime/zval/ (11 files including mod.rs)
These helpers back the PHP zval bridge extension: they convert elephc runtime values (boxed Mixed cells, indexed/hash arrays, strings) into PHP zval / zend_string / zend_array structures and back. String and array children are freshly allocated through __rt_heap_alloc, so a produced zval owns independent PHP-shaped storage.
| Routine | What it does |
|---|---|
__rt_zval_pack | Convert a boxed Mixed cell into a 16-byte zval heap block |
__rt_zval_pack_array_packed / __rt_zval_pack_array_hash | Build zend_array storage from an indexed array / associative hash |
__rt_zval_unpack / __rt_zval_unpack_array | Convert a zval (or zend_array) back into elephc runtime values |
__rt_zval_string_new | Allocate a zend_string from an elephc string |
__rt_zval_djbx33a | The DJBX33A hash used for zend_string key hashing |
__rt_zval_type | Report a zval’s type tag |
__rt_zval_free / __rt_zval_free_array / __rt_zval_free_children | Release packed zval storage, including nested children |
Buffer routines
Source: src/codegen_support/runtime/buffers/ (8 files including mod.rs)
These helpers support the compiler-specific buffer<T> hot-path data type. Public Buffer values are opaque (generation:u32 << 32) | descriptor_index:u32 handles. The runtime resolves each non-null handle through a 4096-slot static descriptor registry and validates its active marker and generation before exposing payload metadata. Payload bytes remain a separate compiler-heap allocation.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_buffer_resolve | Validate handle index, non-zero generation, active state, and exact descriptor generation | x0 = opaque buffer handle | x0 = static descriptor address |
__rt_buffer_try_resolve | Apply the same validation without reporting a fatal error, so loop preheaders can resolve speculatively and the actual access retains the diagnostic | x0 = opaque buffer handle | x0 = descriptor address, or zero for an invalid or stale handle |
__rt_buffer_new | Reuse or claim a descriptor, allocate and exactly zero length * stride payload bytes, then publish the new handle | x0 = element count, x1 = element stride | x0 = opaque buffer handle |
__rt_buffer_free | Invalidate a resolved descriptor, recycle it unless its u32 generation is saturated, then release the detached payload | x0 = opaque buffer handle | — |
__rt_buffer_len | Resolve the handle and read the logical element count from descriptor offset 8 | x0 = opaque buffer handle | x0 = length |
__rt_buffer_bounds_fail | Abort with Fatal error: buffer index out of bounds | — | does not return |
__rt_buffer_new_size_fail | Abort with Fatal error: buffer_new() length is negative or exceeds the maximum buffer size when length * stride is invalid | — | does not return |
__rt_buffer_registry_exhausted | Abort with Fatal error: buffer registry exhausted when no descriptor slot can be issued | — | does not return |
__rt_buffer_use_after_free | Abort with Fatal error: use of buffer after buffer_free() | — | does not return |
Mixed-type helpers
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_mixed_cast_int | Unbox a mixed cell and cast to integer | x0 = mixed cell pointer | x0 = integer |
__rt_mixed_cast_bool | Unbox a mixed cell and cast to boolean | x0 = mixed cell pointer | x0 = 0 or 1 |
__rt_mixed_cast_float | Unbox a mixed cell and cast to float | x0 = mixed cell pointer | d0 = float |
__rt_mixed_cast_string | The string-cast tag dispatch: ints/floats/bools format into the shared concat scratch, strings and OBJECTS are persisted into a block the caller owns (an object renders through its own __toString, resolved in the dense _class_tostring_ptrs table), arrays warn and yield Array, and resources yield Resource id #N | x0 = mixed cell pointer | x1/x2 = string |
__rt_mixed_cast_array | The (array) cast tag dispatch: arrays keep their COW payload, objects project to property hashes, null yields an empty array, and every other tag wraps into a one-element Mixed array | x0 = mixed cell pointer | x0 = array pointer |
__rt_mixed_instanceof | Unbox a mixed cell and test object payloads against class/interface metadata | x0 = mixed cell pointer, x1 = target id, x2 = 0 class / 1 interface | x0 = 0 or 1 |
__rt_instanceof_lookup | Resolve a dynamic class-string target against emitted class/interface name metadata | x1/x2 = string | x0 = found, x1 = target id, x2 = 0 class / 1 interface |
__rt_mixed_is_empty | Check emptiness of a mixed cell (PHP semantics) | x0 = mixed cell pointer | x0 = 0 or 1 |
__rt_mixed_strict_eq | Compare two mixed cells by tag and value | x0, x1 = mixed pointers | x0 = 0 or 1 |
__rt_mixed_unbox | Extract the raw payload from a mixed cell | x0 = mixed cell pointer | x0/x1/x2 depending on type |
__rt_mixed_count | Count boxed indexed arrays and hashes, returning zero for non-countable payloads | x0 = mixed cell pointer | x0 = count |
__rt_iterable_write_stdout | Print iterable arrays and hashes as PHP’s "Array" display string | x0 = iterable heap pointer | — |
__rt_iterable_unsupported_kind | Abort when runtime iterable dispatch sees an unsupported heap kind | — | does not return |
__rt_hash_may_have_cyclic_values | Scan hash entries to check if any contain refcounted children | x0 = hash pointer | x0 = 0 (scalar-only) or 1 (has cycles) |
__rt_match_unhandled | Abort with Fatal error: unhandled match case | — | does not return |
Object and stdClass routines
Weak references use a non-owning heap-granule index, described in the memory model, and invalidate observers before reclaimed storage can be reused. WeakMap key retirement can invoke PHP, so collection resolves resurrection before notifying unreachable keys.
| Weak-observer routine | What it does |
|---|---|
__rt_weak_reference_create | Return the live wrapper for an object or Closure, allocating a wrapper only when one does not already exist |
__rt_weak_reference_get | Read a wrapper’s nullable referent without making the stored weak edge owning |
__rt_weak_reference_retire | Clear forward and reverse weak-reference identity before object or wrapper storage reuse |
__rt_weak_map_notify | Retire native weak-reference observers and invoke the registered WeakMap key-retirement callback when needed |
Source: src/codegen_support/runtime/objects/ (19 files)
These helpers support stdClass, json_decode() object results, boxed Mixed property/index access, object destructor dispatch, and dynamic new $name() instantiation. stdClass instances use a compact [class_id][hash_ptr] payload, with dynamic properties stored in a hash of boxed Mixed values.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_object_to_hash | Project an object’s properties into a string-keyed hash — backs get_object_vars() and (array) object casts through serialize descriptors, with declaring-class protected/private filtering and __PHP_Incomplete_Class handling | object pointer + mode | hash pointer |
__rt_throw_object_not_array | Raise PHP’s catchable Cannot use object of type X as array | object pointer | does not return |
__rt_throw_object_not_array_name | Raise the same catchable Error for a Closure without reading an object header | class name pointer/length (x1/x2 on AArch64, rax/rdx on x86_64) | does not return |
__rt_new_by_name | Instantiate a class by its textual name through the _classes_by_name table (case-insensitive __rt_strcasecmp lookup), allocating and zeroing the object payload | class name string | object pointer, or 0 (null) on miss |
__rt_call_object_destructor | Look up the object’s __destruct in the class_id-indexed _class_destruct_ptrs table and invoke it with $this borrowed before storage is released; guarded against re-entry | object pointer | — |
__rt_stdclass_new | Allocate an empty stdClass object with hash-backed dynamic property storage | stdClass class id from runtime data | object pointer |
__rt_stdclass_from_hash | Wrap a decoded JSON object hash in a stdClass instance | hash pointer | object pointer |
__rt_stdclass_get | Read a dynamic property and return a boxed Mixed value, or Mixed(null) when missing | object pointer + property string | boxed mixed payload |
__rt_stdclass_set | Store a boxed Mixed value into a dynamic property hash | object pointer + property string + boxed value | — |
__rt_mixed_property_get | Unbox a Mixed object payload and dispatch stdClass property reads | boxed mixed + property string | boxed mixed payload |
__rt_mixed_property_set | Unbox a Mixed object payload and dispatch stdClass property writes; every other class silently drops the write, so it is only the FALLBACK behind the call-site class-id dispatch described in the code generator | boxed mixed + property string + boxed value | — |
__rt_mixed_array_get | Unbox Mixed array/hash/stdClass payloads for $mixed[$key] access | boxed mixed + normalized key tuple | boxed mixed payload |
__rt_mixed_array_set | Unbox a Mixed indexed-array/hash payload and write a boxed Mixed value for $mixed[$key] = ...; consumes the boxed value on success | boxed mixed + normalized key tuple + boxed value | — |
__rt_json_encode_stdclass | Encode the dynamic-property hash backing stdClass as a JSON object | stdClass hash pointer | x1/x2 = JSON string |
SPL and iterable routines
Source: src/codegen_support/runtime/spl/ (4 files: fixed_array.rs, doubly_linked_list.rs, offset_convert.rs, mod.rs)
These helpers back SPL container classes whose PHP surface needs custom runtime storage. SplDoublyLinkedList (and its SplStack / SplQueue subclasses) store a class id, an owned indexed array of boxed Mixed cells, an iterator index, and iterator-mode bits. SplFixedArray stores a class id and a fixed-size storage array of owned boxed Mixed cells (or null for unset/null slots). Mutating methods take ownership of the boxed Mixed arguments prepared by call lowering, and resize/overwrite paths release any replaced cell first.
ArrayAccess offsets for both families funnel through __rt_spl_offset_convert, which applies the PHP offset rules of the receiver: a canonical integer string is an index for SplFixedArray while the linked-list family weak-coerces any numeric one (mode 0/1/2 selects the family, and mode 2 gives offsetSet(null) its own append meaning). A rejected offset answers the receiver’s exact Cannot access offset of type <type> / ... must be of type ?int spelling through __rt_spl_throw_offset_type; the owned offset box is released before any diagnostic that may dispatch to a user error handler. _spl_offset_type_rows and _spl_fixed_offset_type_prefix / _spl_fixed_offset_type_suffix / _spl_float_string_prefix / _spl_float_string_suffix hold those spellings and the float-string deprecation words.
SplDoublyLinkedList / SplStack / SplQueue
| Routine | What it does |
|---|---|
__rt_spl_dll_new | Allocate an empty doubly-linked-list object with initial mixed-cell storage |
__rt_spl_dll_push / __rt_spl_dll_pop | Append to / remove from the end |
__rt_spl_dll_unshift / __rt_spl_dll_shift | Prepend to / remove from the front |
__rt_spl_dll_top / __rt_spl_dll_bottom | Peek the last / first element |
__rt_spl_dll_insert | Insert at an index honoring the iterator mode |
__rt_spl_dll_count / __rt_spl_dll_is_empty | Element count / emptiness check |
__rt_spl_dll_set_iterator_mode / __rt_spl_dll_get_iterator_mode | Write / read LIFO/FIFO and DELETE iterator-mode bits |
__rt_spl_dll_rewind / __rt_spl_dll_valid / __rt_spl_dll_current / __rt_spl_dll_key / __rt_spl_dll_next / __rt_spl_dll_prev | Iterator surface honoring the active iterator mode |
__rt_spl_dll_offset_exists / __rt_spl_dll_offset_get / __rt_spl_dll_offset_set / __rt_spl_dll_offset_unset | ArrayAccess operations |
__rt_spl_dll_serialize / __rt_spl_dll_serialize_array / __rt_spl_dll_unserialize | Serialization helpers |
SplFixedArray
| Routine | What it does |
|---|---|
__rt_spl_fixed_new | Allocate a fixed-size array object with a zero-initialized mixed-cell storage block |
__rt_spl_fixed_count | Return the fixed size |
__rt_spl_fixed_set_size | Resize storage, releasing dropped cells and zero-filling new slots |
__rt_spl_fixed_offset_exists / __rt_spl_fixed_offset_get / __rt_spl_fixed_offset_set / __rt_spl_fixed_offset_unset | ArrayAccess operations with bounds checking |
__rt_spl_fixed_to_array / __rt_spl_fixed_from_array / __rt_spl_fixed_copy_from_array | Convert to / build from a PHP array |
__rt_spl_fixed_unserialize | Serialization helper |
Shared SPL offset conversion
| Routine | What it does |
|---|---|
__rt_spl_offset_convert | Convert a boxed mixed offset under the receiver’s rules: mode 0 uses SplFixedArray’s array-key ones (a canonical integer string is an index), mode 1 the linked-list family’s weak int $index coercion with null reading as 0, mode 2 the offsetSet(null) append form. Answers the converted index or, on rejection, the exact type spelling taken from _spl_offset_type_rows; consumes the owned offset box, releasing it before any diagnostic that may reach a user error handler |
__rt_spl_throw_offset_type | Raise TypeError spelling the rejected offset’s type: <prefix><type><suffix> concatenated, persisted, and thrown as a canonical 56-byte Throwable stamped with _spl_type_error_class_id; never returns |
Generator routines
Source: src/codegen_support/runtime/generators/ (3 files: mod.rs, coro.rs, frame.rs)
These helpers back the built-in Generator class. Generators are stackful coroutines that reuse the Fiber runtime: a Generator object reuses the Fiber 232-byte layout (so it can drive itself through __rt_fiber_switch / suspend / resume / throw) plus a small block of generator-specific fields (last_key, last_value, return_value, auto_key, delegated_iter) at offsets 184..224 inside the otherwise-unused Fiber reserved region. The generated generator body runs on its own coroutine stack and calls __rt_gen_suspend at each yield; the accessor helpers below drive the coroutine for the public Iterator surface, send()/throw(), and getReturn().
Because the fiber suspend boundary re-raises a scheduled exception inside the coroutine’s own stack, Generator::throw() lands in an in-generator try/catch (issue #329) rather than unwinding the caller.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_gen_suspend | yield suspension primitive: record the yielded key/value into the generator’s persistent slots (NULL key → auto-increment integer key), then suspend via __rt_fiber_suspend | boxed key cell, boxed value cell | boxed mixed delivered by the next send()/next() |
__rt_gen_current | Return an owned ref to the boxed Mixed value from the most recent yield | Generator* | boxed mixed payload |
__rt_gen_key | Return an owned ref to the boxed Mixed key from the most recent yield | Generator* | boxed mixed key |
__rt_gen_valid | Report whether the generator is not terminated | Generator* | bool |
__rt_gen_next | Resume the coroutine past the current yield unless terminated | Generator* | — |
__rt_gen_send | Store a boxed Mixed sent value, then resume the coroutine | Generator*, boxed mixed value | boxed mixed payload |
__rt_gen_throw | Schedule a pending throw and resume so the exception is re-raised inside the coroutine | Generator*, throwable object | boxed mixed payload or rethrown exception |
__rt_gen_rewind | Run the generator to its first yield once | Generator* | — |
__rt_gen_get_return | Return an owned ref to the boxed terminal return value | Generator* | boxed mixed payload |
__rt_gen_delegate | Drive a yield from delegate stored in delegated_iter, forwarding inner yields to the outer caller until the inner iterator is exhausted | Generator* | — |
Generators are stamped as object heap blocks (heap kind 4) because Generator is a built-in class implementing Iterator. __rt_object_free_deep detects the built-in Generator class id and releases the coroutine’s custom Mixed slots plus any active yield from delegate instead of treating the payload as ordinary class properties.
Fiber routines
Source: src/codegen_support/runtime/fibers/ (4 top-level files plus 4 files in the api/ subdirectory)
These helpers implement PHP 8.1-style cooperative coroutines. They are emitted by the shared runtime on every supported target.
| Routine | What it does | Input | Output |
|---|---|---|---|
__rt_fiber_alloc_stack | Allocate a per-fiber native stack with a protected guard page | requested usable stack size | stack base, initial top, mapped size |
__rt_fiber_free_stack | Return a mapped fiber stack to the OS | stack base, mapped size | — |
__rt_fiber_switch | Save the current callee-saved context and restore the target fiber/main context | target Fiber* or null for main | resumes when this context is switched back to |
__rt_fiber_entry | Trampoline run on first entry to a fiber stack; calls the generated wrapper, records return/escape state, and switches back | active _fiber_current | does not return normally inside the fiber |
__rt_fiber_construct | Allocate and initialize the runtime-managed Fiber object and its initial stack frame | callable descriptor pointer, Fiber class id, generated wrapper pointer | Fiber* |
__rt_fiber_throw_state_error | Allocate a FiberError and throw it through the normal exception runtime | message pointer and length | does not return |
__rt_fiber_start | Start a not-yet-started fiber and return its first yielded value or null on immediate termination | Fiber* | boxed mixed payload |
__rt_fiber_resume | Resume a suspended fiber with a boxed payload | Fiber*, boxed mixed value | boxed mixed payload |
__rt_fiber_suspend | Suspend the current fiber and yield a boxed payload to its caller | boxed mixed value | boxed mixed value supplied by the next resume |
__rt_fiber_throw | Resume a suspended fiber by throwing into its pending suspend point | Fiber*, throwable object | boxed mixed payload or rethrown exception |
__rt_fiber_get_current | Return the currently running fiber, or null when running on the main stack | — | boxed mixed payload |
__rt_fiber_get_return | Read the terminal return payload from a terminated fiber | Fiber* | boxed mixed payload |
__rt_fiber_state_eq | Shared predicate helper for isStarted(), isSuspended(), isRunning(), and isTerminated() | Fiber*, state id | bool |
How routines are emitted
File: src/codegen_support/runtime/emitters.rs
The emit_runtime() function calls the target-aware routine emitters in a fixed order. Each runtime module owns the shared helper surface and dispatches internally when AArch64 and Linux x86_64 need different instruction sequences or ABI setup. A RuntimeFeatures argument gates the optional groups: the regex family is emitted when selected, and mbstring helpers are emitted for direct mbstring use or dynamic eval, the eval bridge/scope helpers only when the final EIR module requires them, and the --web flag selects the web-aware bodies of __rt_stdout_write, __rt_php_input, __rt_http_response_code, and __rt_header.
pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) {
// diagnostics: runtime warning emission and @ suppression state
// numeric: PHP float-to-int coercion and shared rounding-mode decoding
// strings: itoa, resource display/stdout, ftoa, concat, atoi, equality, formatting, trim/mask,
// search/replace, explode/implode, hashing, encoding, sscanf, mbstring (gated), ...
// curl: easy/multi/share handles, callbacks, multipart bodies, errors, version metadata
// bcmath: exact-decimal bridge marshalling and catchable error translation
// callables: dynamic is_callable() fallback, callable-descriptor release, Closure::bind
// system: argv, time, getenv, shell, date/mktime/strtotime, JSON, serialize/unserialize, regex (gated)
// exceptions: cleanup walk, catch matching, class-implements, throw/rethrow helpers
// generators: fiber-backed Generator suspend/current/key/valid/next/send/rewind/throw/getReturn/yield-from
// arrays: heap alloc/free, array/hash helpers, sort, callbacks, refcount, GC
// eval bridge/scope: boxed-value hooks and native eval scope helpers (gated)
// spl: SplDoublyLinkedList/SplStack/SplQueue and SplFixedArray storage helpers
// resource ids: stable PHP-visible resource handle allocation and lookup
// objects/comparison: stdClass, boxed Mixed property/index dispatch, loose equality
// buffers: generation-safe handle resolution, descriptor allocation/free, bounds/size/UAF traps
// io: stdout funnel + web helpers, c-string buffers, file I/O, stat/fs helpers,
// scandir/glob/tempnam, CSV, streams/sockets, var_dump/print_r, ob_* buffer stack
// pointers: ptoa, null check, str_to_cstr, cstr_to_str
// zval: pack/unpack bridge between elephc values and PHP zval structures
// pdo: callable collation/scalar/aggregate adapters (gated by pdo_udf)
// fibers: guarded stack allocation, context switch, entry trampoline, Fiber API
}
Notable runtime-only helpers emitted here include __rt_diag_push_suppression, __rt_diag_pop_suppression, __rt_diag_warning, __rt_exception_cleanup_frames, __rt_exception_matches, __rt_instanceof_lookup, __rt_instanceof_invalid_target, __rt_throw_current, __rt_heap_debug_fail, __rt_heap_kind, __rt_hash_insert_owned, __rt_hash_free_deep, __rt_array_column_ref, __rt_mixed_instanceof, __rt_iterable_write_stdout, __rt_iterable_unsupported_kind, __rt_class_implements_interface, __rt_callable_descriptor_release, __rt_closure_bind, __rt_serialize_value, __rt_unserialize_begin, __rt_spl_dll_new, __rt_spl_fixed_new, __rt_gen_suspend, __rt_gen_current, __rt_gen_send, __rt_preg_strip, __rt_pcre_to_posix, __rt_str_to_cstr, __rt_cstr_to_str, __rt_stdout_write, __rt_vd_write, __rt_pr_write, __rt_ob_start_ex, __rt_ob_apply_handler, __rt_ob_flush_all, __rt_zval_pack, __rt_fiber_switch, and __rt_fiber_entry in addition to the more user-visible helpers.
Internal helper inventory
The tables above document the public runtime operations. The emitters also split complex operations into the following internal symbols so the linker can dead-strip each unit independently:
- Arrays, hashes, and Mixed values:
__rt_abs_mixed,__rt_amr_box_value,__rt_array_edge_key,__rt_array_ensure_elem_for_write,__rt_array_fill_assoc,__rt_array_fill_str,__rt_array_find_any_all,__rt_array_get_mixed_key,__rt_array_is_list,__rt_array_merge_recursive,__rt_array_multisort,__rt_array_replace,__rt_array_replace_recursive,__rt_array_set_int,__rt_array_set_mixed,__rt_array_set_mixed_key,__rt_array_set_refcounted,__rt_array_set_str,__rt_array_sum_mixed,__rt_array_to_hash,__rt_array_udiff_uintersect,__rt_array_walk_recursive,__rt_assoc_diff_intersect,__rt_hash_flip,__rt_hash_map,__rt_hash_sum_mixed,__rt_hash_to_indexed_array,__rt_in_array_mixed_int,__rt_mixed_array_append,__rt_mixed_array_get_for_write,__rt_mixed_cell_autovivify_array,__rt_mixed_cell_promote_to_hash,__rt_mixed_new_empty_array_cell, and__rt_mixed_numeric_common. - Strings, dates, JSON, and serialization:
__rt_concat_append,__rt_date_entry,__rt_json_validate_number,__rt_json_validate_string,__rt_microtime_build_into,__rt_microtime_mixed,__rt_microtime_str,__rt_mktime_shifted,__rt_serialize_begin,__rt_serialize_hash_body,__rt_serialize_indexed_body,__rt_serialize_pstr,__rt_serialize_uint,__rt_unser_at, and__rt_unser_key. - Objects, callables, resources, and zvals:
__rt_box_wrapper_stat_result,__rt_function_exists_lookup,__rt_obj_store_prop,__rt_object_handle_acquire,__rt_object_handle_of,__rt_object_handle_release,__rt_resource_id_mint,__rt_resource_id_of,__rt_resource_type_name,__rt_spl_object_hash,__rt_zval_pack_element. - Files, streams, sockets, and networking:
__rt_addr_is_udp,__rt_build_sockaddr_in6,__rt_chgrp_group,__rt_chown_user,__rt_disk_space,__rt_fd_write,__rt_file_get_contents_maybe_url,__rt_format_sockaddr_in,__rt_format_sockaddr_in6,__rt_format_sockaddr_unix,__rt_fsockopen,__rt_fwrite,__rt_get_int_context_option,__rt_get_string_context_option,__rt_http_build_copy_aarch64,__rt_http_build_copy_x86,__rt_inet6_pton,__rt_inet_addr_parse,__rt_lchgrp_group,__rt_lchown_user,__rt_opendir,__rt_opendir_glob,__rt_path_is_wrapper,__rt_popen,__rt_readdir,__rt_readfile_wrapper,__rt_rewinddir,__rt_servent_load,__rt_stash_connect_host,__rt_stream_wrapper_register, and__rt_stream_wrapper_unregister. - User stream wrappers:
__rt_user_wrapper_dir_closedir,__rt_user_wrapper_dir_readdir,__rt_user_wrapper_dir_rewinddir,__rt_user_wrapper_fclose,__rt_user_wrapper_feof,__rt_user_wrapper_fflush,__rt_user_wrapper_flock,__rt_user_wrapper_fread,__rt_user_wrapper_fseek,__rt_user_wrapper_fstat,__rt_user_wrapper_ftell,__rt_user_wrapper_ftruncate,__rt_user_wrapper_fwrite,__rt_user_wrapper_opendir,__rt_user_wrapper_path_op,__rt_user_wrapper_rename,__rt_user_wrapper_set_option,__rt_user_wrapper_stream_cast,__rt_user_wrapper_url_stat, and__rt_user_wrapper_url_stat_field. - Diagnostics and structured output:
__rt_touch_meta_array,__rt_var_dump_array_bool,__rt_var_dump_array_float,__rt_var_dump_array_int,__rt_var_dump_array_str,__rt_var_dump_close_container,__rt_var_dump_emit_bool_line,__rt_var_dump_emit_float_line,__rt_var_dump_emit_indexed_key,__rt_var_dump_emit_int_line,__rt_var_dump_emit_null_line,__rt_var_dump_emit_object_key,__rt_var_dump_emit_recursion_line,__rt_var_dump_emit_string_key,__rt_var_dump_emit_string_line,__rt_var_dump_emit_uninit_line,__rt_var_dump_indexed,__rt_var_dump_object,__rt_var_dump_open_container,__rt_var_dump_open_object,__rt_var_dump_value,__rt_vd_indent_pop,__rt_vd_indent_push,__rt_vd_obj_count,__rt_vd_obj_desc,__rt_vd_pad,__rt_vd_seen_find,__rt_vd_seen_pop,__rt_vd_seen_push,__rt_warn_array_offset_on_null,__rt_warn_foreach_non_iterable,__rt_warn_nan_coerced_bool, and__rt_warn_undefined_array_key_str. - Additional array and scalar entry points:
__rt_alloc_overflow,__rt_array_chunk_to_hash,__rt_array_count_values,__rt_array_iter_next,__rt_array_key_exists_mixed_key,__rt_array_ptr_key,__rt_array_ptr_seek,__rt_array_ptr_value,__rt_array_slice_to_hash,__rt_array_splice_insert_boxed,__rt_array_splice_insert_refcounted,__rt_array_splice_insert_str,__rt_array_splice_insert_unboxed,__rt_array_strict_eq,__rt_array_to_hash_reverse,__rt_count_values_bump,__rt_hash_count_values,__rt_int_pow_checked,__rt_min_max_hash,__rt_min_max_mixed,__rt_min_max_str,__rt_mixed_clone,__rt_mixed_inc_dec,__rt_mixed_intval_base,__rt_mixed_numeric_pow,__rt_php_float_to_int,__rt_php_truthy, and__rt_round_mode. - Additional string and crypto entry points:
__rt_base_convert,__rt_base_to_number,__rt_chunk_split,__rt_concat_grow,__rt_concat_publish,__rt_concat_reserve,__rt_count_chars,__rt_dec_to_base,__rt_openssl_cipher_iv_length,__rt_openssl_decrypt,__rt_openssl_encrypt,__rt_openssl_get_cipher_methods,__rt_parse_url_key_address,__rt_parse_url_throw_component,__rt_quotemeta,__rt_str_inc_dec,__rt_str_to_int_base,__rt_str_word_count,__rt_strncasecmp,__rt_strncmp,__rt_strtr_array,__rt_strtr_hash,__rt_strtr_int_key_len,__rt_strtr_pairwise,__rt_strtr_probe, and__rt_substr_count. - Additional object, generator, I/O, bridge, and PDO entry points:
__rt_bcmath_throw,__rt_file_get_contents_range,__rt_gen_suspend_delegated,__rt_obj_dump_dyn_props,__rt_obj_dyn_prop_at,__rt_obj_enum_case_name,__rt_obj_prop_count,__rt_obj_prop_name,__rt_pdo_call_agg_final,__rt_pdo_call_agg_step,__rt_pdo_call_collation,__rt_pdo_call_scalar,__rt_pr_obj_desc,__rt_print_r_hash_entries,__rt_print_r_object, and__rt_var_dump_emit_enum_line. - Boxed arrays, hashes, references, and properties:
__rt_array_cell_ensure_unique,__rt_array_column_boxed,__rt_array_flip_boxed,__rt_array_flip_boxed_body,__rt_array_map_boxed,__rt_array_merge_boxed,__rt_array_multisort_boxed,__rt_array_reduce_boxed,__rt_array_reverse_boxed,__rt_array_set_mixed_key_already_diagnosed,__rt_array_take_boxed,__rt_array_walk_boxed,__rt_array_walk_boxed_visit,__rt_hash_append_error,__rt_hash_chunk,__rt_hash_entry_deref,__rt_hash_entry_make_reference,__rt_hash_iter_next_value,__rt_hash_iter_resync,__rt_hash_iter_resync_successor,__rt_hash_next_index,__rt_hash_pop_boxed,__rt_hash_project_spread,__rt_hash_set_value,__rt_hash_try_next_index,__rt_hash_write_guard_claim,__rt_hash_write_guard_pop,__rt_hash_write_guard_push,__rt_in_array_boxed,__rt_local_ref_cell_release,__rt_mixed_deref,__rt_reference_cell_clone,__rt_reference_cell_free_deep,__rt_reference_cell_is_unmanaged_borrow,__rt_reference_cell_new,__rt_reference_cell_owner,__rt_reference_cell_release,__rt_reference_cell_value_release,__rt_reference_is,__rt_reference_new,__rt_reference_replace,__rt_property_hash_get,__rt_property_hash_set,__rt_object_clone_shallow_boxed, and__rt_mixed_str_operand_is_float. - Argument binding and mbstring bridge adapters:
__rt_call_argument_collect_named,__rt_call_argument_collect_positionals,__rt_call_argument_validate_unpack,__rt_mbregex_init,__rt_mbstring_array_next_impl,__rt_mbstring_build_array,__rt_mbstring_build_ini_array,__rt_mbstring_capture_reference_fill,__rt_mbstring_capture_reference_store,__rt_mbstring_catalog,__rt_mbstring_clone,__rt_mbstring_drop_array,__rt_mbstring_exception_chain,__rt_mbstring_float,__rt_mbstring_header_status,__rt_mbstring_ini_input,__rt_mbstring_output_header,__rt_mbstring_pin,__rt_mbstring_publish_throw,__rt_mbstring_query_hash_store_array,__rt_mbstring_query_next,__rt_mbstring_query_register,__rt_mbstring_query_release,__rt_mbstring_query_root,__rt_mbstring_release_catalog,__rt_mbstring_request_reset,__rt_mbstring_restore_array, and__rt_mbstring_restore_ini_array. - GC, cleanup, exceptions, and diagnostics:
__rt_backtrace_print_arg,__rt_call_object_destructor_body,__rt_cleanup_call,__rt_cleanup_chain_previous,__rt_cleanup_invoke,__rt_cleanup_preserve_exception,__rt_destructor_throw_mixed,__rt_diag_reset,__rt_diag_write,__rt_dispatch_uncaught_exception,__rt_error_handler_invoke,__rt_error_handler_restore,__rt_exception_chain,__rt_gc_collect_cycles_explicit,__rt_gc_collector_begin,__rt_gc_collector_end,__rt_gc_destructor_begin,__rt_gc_destructor_end,__rt_gc_destructors,__rt_gc_disable,__rt_gc_drop_pins,__rt_gc_enable,__rt_gc_enabled,__rt_gc_eval_object_children,__rt_gc_free_begin,__rt_gc_mem_caches,__rt_gc_protected_destructor,__rt_gc_request_start,__rt_gc_rethrow_pending,__rt_gc_status_metric,__rt_gc_unpin_reachable,__rt_heap_debug_live_blocks,__rt_magic_set_guard_pop,__rt_magic_set_guard_push,__rt_throw_boxed_destructor_exception,__rt_throwable_append_previous,__rt_throwable_box_owned,__rt_throwable_initialize,__rt_throwable_previous,__rt_throwable_previous_slot,__rt_throwable_take_boxed,__rt_warning_cleanup, and__rt_wrapper_unbox_int. - Curl and iconv bridge adapters:
__rt_curl_easy_body,__rt_curl_easy_str_op,__rt_curl_invoke_callback,__rt_curl_rethrow_pending,__rt_curl_version,__rt_iconv_build_array,__rt_iconv_call,__rt_iconv_call_bool,__rt_iconv_invoke,__rt_iconv_materialize,__rt_iconv_mime_option,__rt_iconv_option_table, and__rt_iconv_release_block. - Serialization and formatting internals:
__rt_implode_cast_string,__rt_serialize_magic_body,__rt_serialize_sleep_body,__rt_sprintf_throw_string_error,__rt_sprintf_throw_string_error_x64,__rt_sprintf_warn_array_to_string,__rt_sprintf_warn_array_to_string_x64,__rt_sprintf_warn_object_numeric,__rt_sprintf_warn_object_numeric_x64,__rt_str_numeric_value,__rt_unser_allowed_object_to_string,__rt_unser_throw_object_string_error,__rt_unser_throw_type_error,__rt_unser_validate_at,__rt_unser_validate_key,__rt_unser_validate_uint,__rt_unserialize_class_allowed,__rt_unserialize_defer_data,__rt_unserialize_detach_data,__rt_unserialize_end,__rt_unserialize_finish_data,__rt_unserialize_register_array,__rt_unserialize_set_options,__rt_unserialize_set_options_indexed, and__rt_unserialize_set_options_mixed. - Resources, eval objects, files, and display:
__rt_eval_object_release_children,__rt_file_put_contents_maybe_phar_flagged,__rt_get_resources,__rt_ob_release_eval_handler,__rt_resource_inventory_close,__rt_resource_inventory_insert,__rt_resource_inventory_register,__rt_resource_inventory_reset,__rt_resource_inventory_retire,__rt_resource_type_selector, and__rt_var_dump_emit_callable.
Compiled executables dead-strip unreachable runtime helpers at link time. On Linux each __rt_* helper is emitted in its own .text.<name> section and collected with --gc-sections; on macOS the runtime object carries a .subsections_via_symbols footer so each helper is a separately collectable atom dropped by -dead_strip (internal cross-helper labels stay assembler-local L-locals, with the few helpers reached by a b/bl from another atom marked .alt_entry so they remain live symbols). Combined with the AST-side control-flow pruning and dead-code elimination elephc already does before codegen, only the helpers a program actually reaches are linked. Shared libraries (--emit cdylib) keep the full runtime so every exported entry stays callable.
The runtime can also be emitted in position-independent mode for --emit cdylib builds: the emitter’s pic_data_refs flag makes the abi::symbols helpers route every global data reference through the GOT (@GOTPCREL on x86_64, :got:/:got_lo12: on AArch64) instead of direct PC-relative addressing. A separate cdylib_boundary flag controls exception activations and recoverable runtime exits, so those semantics no longer depend implicitly on PIC. Every internal ELF global receives .hidden; Mach-O uses .private_extern. The PIC and non-PIC variants produce different assembly text, so they cache as separate runtime objects.
The cdylib boundary also makes recoverable unwinding explicit. Each cdylib user function publishes an exception-activation record that links the current frame to its generated cleanup callback. If an exception escapes any export, the existing Throwable machinery walks those records, releases function-owned values, and returns through the boundary handler. Heap and concat allocation exhaustion use the same active boundary to report ELEPHC_STATUS_ALLOCATION_FAILURE; other shared emit_exit paths become ELEPHC_STATUS_RUNTIME_FAILURE while a boundary is active, and executable fatal paths remain unchanged. Scalar wrappers preserve their original return signatures and expose the recorded result through elephc_last_status; owned-string wrappers preserve any fixed supported input layout, return status directly, copy successful bytes into independently owned runtime-heap storage, and transfer that buffer to the C caller for elephc_free. Boundary depth is counted, and each wrapper saves/restores _concat_off, so nested teardown cannot disable or corrupt its caller’s boundary state. Diagnostic presence is stored separately from diagnostic length, so an empty Throwable message remains distinguishable from “no error”. See The Codegen and Shared Libraries.
Runtime data
The runtime data layer lives in src/codegen_support/runtime/data/. fixed.rs emits shared buffers, error strings, and lookup tables; user.rs emits per-program globals, statics, enum-case slots, and metadata tables; instanceof.rs formats dynamic instanceof lookup names. Together they declare global buffers using .comm and static data tables:
.comm _concat_buf, 65536 ; 64KB string buffer
.comm _concat_off, 8 ; current offset into string buffer
.comm _print_r_mode, 8 ; print_r($v, true) return-mode capture flag
.comm _print_r_off, 8 ; print_r capture write offset
.comm _print_r_buf, 65536 ; 64KB print_r return-mode capture buffer
.comm _ob_level, 8 ; output-buffer (ob_*) stack depth
.comm _ob_ptrs, 512 ; 64-slot ob buffer base pointers
.comm _ob_lens, 512 ; 64-slot ob buffer used byte counts
.comm _ob_caps, 512 ; 64-slot ob buffer capacities
.comm _ob_handler_stubs, 512 ; 64-slot output-handler invocation stubs
.comm _ob_handler_envs, 512 ; 64-slot handler env words (descriptor ptr / eval registry id)
.comm _ob_name_ptrs, 512 ; 64-slot handler display-name pointers
.comm _ob_name_lens, 512 ; 64-slot handler display-name lengths
.comm _ob_chunk_sizes, 512 ; 64-slot auto-flush chunk sizes
.comm _ob_flags, 512 ; 64-slot ob_start() flags words
.comm _ob_started, 512 ; 64-slot handler-started flags
.comm _ob_in_handler, 8 ; non-zero while a user output handler runs (output discarded)
.comm _ob_flushing, 8 ; process-exit drain re-entry guard
.comm _ob_implicit_flush, 8 ; stored ob_implicit_flush() flag (semantically inert)
.comm _elephc_eval_ob_handler_fn, 8 ; magician hook for eval-registered ob handlers
.comm _elephc_eval_dynamic_object_destruct_fn, 8 ; eval-bridge dynamic-object destructor hook
.comm elephc_web_capture, 8 ; --web output-capture flag (per-target C-ABI symbol mangling)
.comm _global_argc, 8 ; saved argc from OS
.comm _global_argv, 8 ; saved argv pointer from OS
.comm _exc_handler_top, 8 ; top of the active exception-handler stack
.comm _exc_call_frame_top, 8 ; top of the activation-record cleanup stack
.comm _exc_value, 8 ; currently propagating exception object
.comm _fiber_current, 8 ; currently running Fiber object, or null on main
.comm _fiber_main_saved_sp, 8 ; saved main-stack pointer while running a fiber
.comm _fiber_main_saved_exc, 8 ; saved main exception-handler chain while running a fiber
.comm _fiber_main_saved_call_frame, 8 ; saved main cleanup-frame chain while running a fiber
.comm _rt_diag_suppression, 8 ; nested runtime warning-suppression depth for @
.comm _heap_buf, 8388608 ; 8MB heap by default (--heap-size overrides)
.comm _heap_off, 8 ; current heap offset
.comm _heap_free_list, 8 ; head of the general address-ordered free list
.comm _heap_small_bins, 32 ; 4 x 8-byte heads for <=8/16/32/64-byte cached blocks
.comm _heap_debug_enabled, 8 ; BSS-backed debug flag, set to 1 in _main when compiled with --heap-debug
.comm _buffer_registry, 196656 ; reserved slot 0 + 4096 generation-safe 48-byte descriptors
.comm _buffer_registry_free, 8 ; recycled descriptor-index free-list head
_buffer_registry_next:
.quad 1 ; next never-issued descriptor index
.comm _web_heap_guard_enabled, 8 ; enables per-request heap leak checks in --web mode
.comm _gc_collecting, 8 ; cycle collector re-entry guard
.comm _gc_release_suppressed, 8 ; suppress nested collection during deep frees
.comm _json_last_error, 8 ; last JSON_ERROR_* code
.comm _json_active_flags, 8 ; active JSON flags for encode/decode/validate
.comm _json_active_depth, 8 ; current recursive JSON container depth
.comm _json_indent_depth, 8 ; current JSON_PRETTY_PRINT formatting depth
.comm _json_depth_limit, 8 ; configured JSON depth limit
.comm _json_validate_idx, 8 ; validator cursor index
.comm _json_validate_ptr, 8 ; validator input pointer
.comm _json_validate_len, 8 ; validator input length
.comm _json_decode_assoc, 8 ; json_decode object-shape selector
.comm _json_error_source_ptr, 8 ; original decode/validate input pointer
.comm _json_error_source_len, 8 ; original decode/validate input length in bytes
.comm _json_error_candidate_ptr, 8 ; absolute candidate byte, noted in O(1)
.comm _json_error_location_active, 8 ; whether line/column should be appended
.comm _json_error_line, 8 ; one-based line for the last JSON error (zero for empty input)
.comm _json_error_column, 8 ; one-based column for the last JSON error (zero for empty input)
.comm _json_error_scan_count, 8 ; completed diagnostic location scans
.comm _json_error_scan_bytes, 8 ; source bytes loaded by those scans
_heap_max:
.quad 8388608 ; configured heap size limit
.comm _gc_allocs, 8 ; allocation counter
.comm _gc_frees, 8 ; free counter
.comm _gc_live, 8 ; current live heap footprint in bytes
.comm _gc_peak, 8 ; high-water mark counter
.comm _cstr_buf, 4096 ; 4KB C-string conversion buffer
.comm _cstr_buf2, 4096 ; 4KB second C-string buffer
.comm _eof_flags, 256 ; EOF flag per file descriptor
.comm _principal_lookup_buf, 4096 ; passwd/group lookup line buffer
.comm _elephc_crypto_cipher_iv_length_fn, 8 ; published crypto bridge IV-length callback
.comm _elephc_crypto_cipher_methods_fn, 8 ; published crypto bridge cipher-list callback
.comm _elephc_crypto_decrypt_fn, 8 ; published crypto bridge decryption callback
.comm _elephc_crypto_encrypt_fn, 8 ; published crypto bridge encryption callback
_etc_passwd_path:
.asciz "/etc/passwd" ; passwd database path for name lookups
_etc_group_path:
.asciz "/etc/group" ; group database path for name lookups
_principal_lookup_read_mode:
.asciz "r" ; fopen() mode for principal lookup files
; Per-program: global variable storage (one per `global $var` used)
.comm _gvar_x, 16 ; 16 bytes per global variable
; Per-program: static variable storage (one pair per `static $var`)
.comm _static_func_var, 16 ; 16 bytes for persisted value
.comm _static_func_var_init, 8 ; 8-byte initialization flag
; Per-program: static property storage (one slot per effective declaring class property)
.comm _static_prop_Class_prop, 16 ; 16 bytes for the static property value
Additionally, the runtime emits static data tables:
_fmt_g— printf format string for float-to-string conversion via%.14G_b64_encode_tbl— 64-byte Base64 encoding lookup table_b64_decode_tbl— 256-byte Base64 decoding lookup table_spl_autoload_exts_default,_spl_autoload_exts_ptr,_spl_autoload_exts_len— mutable SPL autoload extension state_heap_err_msg,_arr_cap_err_msg,_ptr_null_err_msg— fatal runtime error strings_buffer_bounds_msg,_buffer_uaf_msg,_buffer_alloc_size_msg,_buffer_registry_exhausted_msg,_match_unhandled_msg,_static_prop_private_access_msg,_instanceof_target_type_msg,_iterable_unsupported_kind_msg— fatal runtime error strings for buffers,match, late-bound private static-property access, dynamicinstanceoftarget validation, and iterable dispatch_heap_dbg_bad_refcount_msg,_heap_dbg_double_free_msg,_heap_dbg_free_list_msg— fatal heap-debug error strings enabled by--heap-debug_heap_dbg_*summary labels — fixed strings used by__rt_heap_debug_reportfor alloc/free/live/leak output_resource_id_prefix— prefix used by resource display helpers_pr_spaces,_pr_open,_pr_close— the 64-space padding block and(\n/)\nliterals used by theprint_rwalkers_ob_handler_name,_ob_closure_invoke_name,_ob_k_*— the default-handler /Closure::__invokedisplay names and status-array key strings used byob_get_status()andob_list_handlers()_ob_ntc_*,_ob_warn_bad_callback_*,_ob_fatal_in_handler— PHP-parityob_*notice, warning, and fatal texts, routed through__rt_stdout_writeso parent buffers capture them like PHP_uncaught_exc_msg— fatal exception string written by__rt_throw_currentwhen no handler exists_diag_fopen_failed_msg,_diag_file_get_contents_failed_msg,_diag_define_already_defined_msg— suppressible runtime warning text routed through__rt_diag_warning_fiber_msg_already_started,_fiber_msg_not_suspended,_fiber_msg_throw_not_suspended,_fiber_msg_not_terminated,_fiber_msg_suspend_outside,_fiber_msg_unsupported_callable,_fiber_msg_stack_alloc_failed— messages used byFiberErrorruntime paths_fiber_class_id,_fiber_error_class_id— per-program class ids used by Fiber object cleanup andFiberErrorconstruction_generator_class_id— per-program class id used to recognize Generator frames during object deep-free_php_uname_mode_len_msg,_php_uname_mode_value_msg— fatalphp_uname()argument diagnostics for invalid mode strings_filetype_*,_stat_key_*,_dirname_*,_pathinfo_key_*,_parse_url_*,_tmpfile_template— file metadata, path, URL-component, stat-array, and temporary-file lookup strings used by runtime helpers_locale_utf8_name,_locale_env_name— locale selectors used by runtime helpers that need host locale fallback_json_true,_json_false,_json_null— JSON keyword strings used by__rt_json_encode_booland__rt_json_encode_null_json_int_max_str,_json_int_min_str— decimal threshold strings used byJSON_BIGINT_AS_STRINGoverflow detection without wrapping through integer parsing_json_err_msg_0…_json_err_msg_10,_json_err_msg_table,_json_err_msg_count,_json_err_loc_prefix,_json_err_loc_colon—json_last_error_msg()lookup data and location-suffix fragments for the supportedJSON_ERROR_*code range_day_names— 7 entries (84 bytes), each 12 bytes: day name padded to 10 chars + 1 length byte + 1 padding byte. Used by__rt_dateforl(full name) andD(abbreviated) format characters_month_names— 12 entries (144 bytes), same layout as day names. Used by__rt_dateforF(full name) andM(abbreviated) format characters_strtotime_keyword_tab,_strtotime_unit_tab— keyword, weekday, modifier, and unit lookup tables used by__rt_strtotime_instanceof_target_count,_instanceof_target_entries,_instanceof_name_*— case-insensitive class/interface name metadata used by dynamicinstanceofstring targets, including leading-backslash aliases_class_gc_desc_count,_class_gc_desc_ptrs,_class_gc_desc_<id>— per-class property traversal metadata used by object deep-free and cycle collection_class_json_desc_ptrs,_class_json_desc_<id>,_class_json_pname_<id>_<slot>,_json_exception_class_id,_stdclass_class_id— JSON object encoding descriptors, JsonException construction metadata, and stdClass runtime class id_class_attribute_count,_class_attribute_ptrs,_class_attributes_<id>— emitted class-level PHP attribute metadata. The current PHP-facing helpers and class/enum Reflection attribute constructors materialize their results from the sameClassInfometadata during codegen, rather than doing dynamic runtime class/member lookup._class_vtable_ptrs,_class_vtable_<id>— per-class virtual-method tables used by inheritance dispatch throughclass_id_class_static_vtable_ptrs,_class_static_vtable_<id>— per-class static-method tables used by late static binding_class_destruct_ptrs— class_id-indexed__destructmethod pointers (or0) consulted by__rt_call_object_destructorduring object deep-free_classes_by_name,_classes_by_name_count— case-insensitivename -> (class_id, object size)lookup table used by__rt_new_by_namefornew $variable()static_property_symbol(...)-derived.commslots — 16-byte storage slots for effective declaring static properties, shared by inherited static properties until a subclass redeclares the propertyenum_case_symbol(...)-derived.commslots — singleton backing storage for enum cases emitted from user program metadata
When --heap-debug is enabled, the runtime also activates __rt_heap_debug_check_live, __rt_heap_debug_validate_free_list, and __rt_heap_debug_report. These helpers turn allocator corruption into immediate fatal errors for duplicate frees, zero-refcount incref/decref paths, and malformed free-list or small-bin state, poison freed payload bytes with 0xA5, and print an end-of-process summary with alloc/free counts, live block count, live bytes, leak summary, and the peak live-byte watermark.
Every heap allocation now also carries a uniform 8-byte kind tag in its 16-byte allocator header. The current runtime uses 0=raw/untyped, 1=string, 2=indexed array, 3=assoc/hash, 4=object, 5=boxed mixed, and 6=throwable (the compact runtime-thrown exception/error payloads, accepted by the release dispatchers and routed through the object release path), which lets runtime dispatch stay independent from each payload’s internal layout. Generator frames use heap kind 4 because Generator is a built-in object with a custom payload layout. On x86_64 the kind word additionally carries the ASCII marker "ELPH" in its high 32 bits; every stamp is built through the shared codegen_support::sentinels helpers (x86_64_heap_kind_word() / X86_64_HEAP_MAGIC_HI32) rather than hand-typed immediates, and the refcount/free helpers ignore pointers whose header lacks the marker. The low 16 bits keep the persistent container metadata: low byte = heap kind, bits 8..14 = indexed-array runtime value_type, and bit 15 = copy-on-write container flag. The collector reuses higher bits for transient reachable/incoming-edge metadata during __rt_gc_collect_cycles. Runtime data also now includes _gc_collecting, _gc_release_suppressed, _class_gc_desc_count, _class_gc_desc_ptrs, _class_vtable_ptrs, _class_static_vtable_ptrs, and static-property storage slots so deep-free / cycle-collection paths can coordinate nested releases, discover class property traversal metadata, and support inherited instance dispatch, static-property reads/writes, and late static binding.
See Memory Model for details on how these buffers work.