The roadmap stays in the 0.x series while the compiler, runtime, and product shape are still moving. Current work is focused on PHP parity, backend foundations, and concrete product tracks without a major-version release gate.
Current direction:
- Finish the well-bounded PHP-visible compatibility gaps on the EIR backend.
- Keep completed historical items in their original version sections.
- Move optimizer work behind EIR, benchmark evidence, and real-world validation.
- Treat shared libraries, the PHP extension bridge, and WebAssembly as later 0.x product tracks.
- Leave the major-version discussion for the final future-perspective section.
- Lexer, parser (Pratt), type checker, ARM64 codegen pipeline
- Integers, strings (double and single quoted), echo, variables, comments
- Arithmetic (
+,-,*,/,%), comparison (==,!=,<,>,<=,>=) - String concatenation (
.) with automatic int coercion -
if/elseif/else,while,for,do...while,break,continue, including multi-levelbreak N/continue N - Functions with local scope, return, recursion, nested calls
- Pre/post increment/decrement (
++$i,$i++,--$i,$i--) - Logical operators:
&&,||,and,or,xor,!(and/or/symbolic forms use short-circuit evaluation) - Assignment operators:
+=,-=,*=,/=,.=,%= - Boolean literals:
true,false(as integer 1/0) - Ternary operator:
$x = $a > $b ? $a : $b; -
$argc/$argvsuperglobals -
exit($code);/die(); - Built-in
strlen(),intval() - Error messages with line/column numbers
- Indexed arrays:
$arr = [1, 2, 3]; - Array access, assignment, push:
$arr[0],$arr[0] = 42,$arr[] = "new" -
count(),array_push(),array_pop() -
foreach ($arr as $value) { }loop -
in_array(),array_keys(),array_values(),sort(),rsort(),isset() - Heap allocator (1MB bump allocator)
- Proper null:
echo nullprints nothing,is_null(), null coercion in operations
Proper type system for PHP compatibility.
-
true/falseas distinct Bool type -
echo falseprints nothing,echo trueprints1(like PHP) - Bool coercion:
false→0/""in arithmetic/concat,true→1/"1" -
is_bool(),boolval() -
===and!==strict comparison (type-aware)
- Float literals:
3.14,1.0e-5,-0.5 - Division returns float:
10 / 3→3.3333... -
intdiv()for integer division - Mixed int/float arithmetic (auto-promotion to float)
- Float comparison and formatting
-
floatval(),is_float(),is_int(),is_string(),is_numeric() -
INF,NAN,is_nan(),is_finite(),is_infinite()
- Type casting:
(int),(string),(float),(bool),(array) -
gettype(),settype() -
empty()— check if variable is empty/falsy -
unset()— destroy variable
-
abs(),min(),max(),floor(),ceil(),round() -
sqrt(),pow() -
**exponentiation operator -
fmod(),fdiv() -
rand(),mt_rand(),random_int() -
number_format() - Constants:
PHP_INT_MAX,PHP_INT_MIN,PHP_FLOAT_MAX,M_PI
- String interpolation:
"Hello $name" -
substr(),strpos(),strrpos(),strstr() -
str_replace() -
str_ireplace(),substr_replace() -
strtolower(),strtoupper(),ucfirst(),lcfirst() -
ucwords() -
trim(),ltrim(),rtrim() -
str_repeat(),strrev() -
str_pad() -
explode(),implode() -
str_split() -
sprintf(),printf(),sscanf() -
strcmp(),strcasecmp(),str_contains(),str_starts_with(),str_ends_with() -
ord(),chr() -
nl2br(),wordwrap() -
addslashes(),stripslashes() -
htmlspecialchars(),htmlentities(),html_entity_decode() -
urlencode(),urldecode(),rawurlencode(),rawurldecode() -
md5(),sha1(),hash(),hash_hmac(),hash_file(),hash_equals(),hash_algos(),hash_init()/hash_update()/hash_final()/hash_copy()— full PHP hash family (sha2/sha3/ripemd/whirlpool/crc32/crc32c/adler32/fnv/joaat and more), backed by the pure-Rustcrates/elephc-cryptostaticlib (RustCrypto). Replaces the macOS-CommonCrypto / Linux-libcrypto system-crypto fork: zero system crypto dependency on every target. -
base64_encode(),base64_decode() -
bin2hex(),hex2bin() -
ctype_alpha(),ctype_digit(),ctype_alnum(),ctype_space()
-
fgets(STDIN)/readline()— read from keyboard -
STDIN,STDOUT,STDERRconstants -
fopen(),fclose(),fread(),fwrite(),fgets(),feof() -
fgetcsv(),fputcsv() -
fseek(),ftell(),rewind() -
file_get_contents(),file_put_contents() -
file()— read file into array -
file_exists(),is_file(),is_dir(),is_readable(),is_writable() -
filesize(),filemtime() -
copy(),rename(),unlink(),mkdir(),rmdir() -
scandir(),glob(),getcwd(),chdir() -
tempnam(),sys_get_temp_dir() - Statement-form
printoutput -
var_dump(),print_r()for debugging
- Multi-dimensional arrays:
[[1,2],[3,4]],$a[0][1] - Associative arrays:
$map = ["key" => "value"]; -
foreach ($map as $key => $value) { } - Hash table runtime for string keys
-
array_key_exists(),array_search() -
array_merge(),array_slice(),array_splice() -
array_map(),array_filter(),array_reduce(),array_walk()(string callbacks) -
array_combine(),array_flip(),array_reverse(),array_unique() -
array_column() -
array_sum(),array_product() -
array_chunk(),array_pad(),array_fill(),array_fill_keys() -
array_diff(),array_intersect(),array_diff_key(),array_intersect_key() -
array_unshift(),array_shift() -
usort(),uksort(),uasort()(string callbacks) -
asort(),arsort(),ksort(),krsort() -
natsort(),natcasesort(),shuffle(),array_rand() -
range() - Direct indexed-array growth preserves existing slots for writes such as
$items[2] = 1after$items = [10, 20] -
switch/case/default(with fall-through) -
matchexpression (PHP 8 style, no fall-through)
-
define()/constconstants -
global $var;keyword - Static variables:
static $counter = 0; - Pass by reference:
function foo(&$x) { } - Default parameter values:
function foo($x = 10) { } - Variadic functions:
function foo(...$args) { } - Anonymous functions / closures:
$fn = function($x) { }withuse ($var)captures - Arrow functions:
$fn = fn($x) => $x * 2 - Null coalescing:
$x ?? $default,$x ??= $default - Spread operator:
func(...$args),[...$a, ...$b] - List unpacking:
[$a, $b] = $array; - Heredoc / nowdoc strings
- Bitwise operators:
&,|,^,~,<<,>> - Full compound assignment family:
**=,&=,|=,^=,<<=,>>= - Assignment expressions — local variables and stabilized non-local targets (
$items[0],$items[idx()],$obj->x,makeBox()->x,ClassName::$x, property array slots) support=, compound assignment, and??=as PHP-compatible expressions, including RHS-mutated target dependencies such as$items[$i] ??= ($i = 1), with assignment precedence below?:/??and aboveand/xor/or - Spaceship operator:
<=> -
call_user_func()(string callbacks) -
call_user_func_array() -
function_exists()
-
time(),microtime() -
date(),mktime(),strtotime() -
sleep(),usleep() -
json_encode(),json_decode(),json_last_error()(basic surface) - Extended JSON surface:
json_encode($value, $flags, $depth),json_decode($json, $associative, $depth, $flags),json_last_error_msg(),json_validate()(signatures, structural decode, depth/flag handling, validation, and JSON error state) - All PHP
JSON_*flag andJSON_ERROR_*constants exposed - Object encoding via public properties +
JsonSerializabledispatch (including nested objects, arrays of objects, and assoc-of-objects) -
JsonException+RuntimeExceptionclass hierarchy (catchable as themselves and any parent class) -
JSON_PRETTY_PRINT(4-space indent, newlines between elements, single space after:) andJSON_UNESCAPED_SLASHES -
JSON_HEX_TAG,JSON_HEX_AMP,JSON_HEX_APOS,JSON_HEX_QUOT(hex-escape</>,&,',"for HTML/XML embedding contexts) -
JSON_FORCE_OBJECT(indexed arrays encode as{"0":val,...}; specialized array_int / array_str fast paths runtime-redirect to array_dynamic when the flag is on) -
JSON_PRESERVE_ZERO_FRACTION(integer-valued floats encode as1.0instead of collapsing to1; tail-appends.0when the formatted slice has no./e/Emarker) -
JSON_UNESCAPED_UNICODE(multibyte UTF-8 escaped to\uXXXXby default, with surrogate-pair encoding for codepoints ≥ U+10000; flag preserves the literal bytes); ARM64 + x86_64 paths both implemented (Linux runtime parity validated via Docker scripts). -
JSON_NUMERIC_CHECK(numeric strings encode as raw JSON numbers when the entire input matches the RFC 8259 number grammar;array_strredirects toarray_dynamicso the per-element check fires inside indexed string arrays). -
$depthenforcement: each container encoder (assoc, indexed array, object) increments_json_active_depthat entry, compares with_json_depth_limit, triggersJSON_ERROR_DEPTH(andJsonExceptionunderJSON_THROW_ON_ERROR) when the limit is crossed, and decrements on exit so siblings start fresh. -
JSON_THROW_ON_ERRORforjson_encode()andjson_decode()errors (raisesJsonExceptionwith PHP-compatible messages);json_validate()follows PHP's allowed flag set and rejectsJSON_THROW_ON_ERROR -
json_validate()recursive-descent RFC 8259 validator: literals (null/true/false), number grammar (-?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?), string escapes (\",\\,\/,\b,\f,\n,\r,\t,\uHHHH), balanced arrays/objects, colon between key and value, no trailing content; depth tracked against$depthand routed through__rt_json_throw_error(JSON_ERROR_DEPTHfor overflow,JSON_ERROR_SYNTAXfor any malformed token). - Inf/NaN detection in
json_encode()(setsJSON_ERROR_INF_OR_NAN, returnsfalseby default, substitutes0withJSON_PARTIAL_OUTPUT_ON_ERROR, and throws withJSON_THROW_ON_ERROR) - List-shape detection in associative-array encoder: hashes whose keys form
0..count-1in insertion order emit JSON arrays ([...]), matching PHP's runtime detection.JSON_FORCE_OBJECToverrides; empty hashes encode as[]. - Malformed UTF-8 detection in
json_encode()(lead-byte validation, continuation-byte validation, bounds-checked truncated sequences) honoringJSON_INVALID_UTF8_IGNORE(silent drop),JSON_INVALID_UTF8_SUBSTITUTE(emit�), andJSON_THROW_ON_ERROR(raisesJsonExceptionforJSON_ERROR_UTF8) -
JSON_PARTIAL_OUTPUT_ON_ERRORsemantics: encoder errors returnfalseby default; when the flag is set, substitutable failures such as Inf/NaN keep partial output (0) while malformed UTF-8 is handled by the explicit ignore/substitute flags -
json_decode()returning a fully structuredMixedvalue: scalars (null, bool, int, float, string with full escape decoding), empty containers, non-empty arrays (recursive-descent with depth-and-string-aware boundary scanner; each element recursively decodes via__rt_json_decode_mixed), and non-empty objects (recursive: keys parsed as JSON strings, values recursively decoded, pairs inserted into a hash via__rt_hash_set). -
stdClassbuiltin class with dynamic property storage.new stdClass()allocates a 16-byte object whose hidden hash backs$obj->name = $val/$obj->name. Property access onstdClass(and onMixedreceivers — the commonjson_decode($json)->nameidiom) routes through__rt_stdclass_get/__rt_stdclass_set(and__rt_mixed_property_get/__rt_mixed_property_setfor the unbox+dispatch path).json_decode($json)returnsstdClassby default (PHP semantics),json_decode($json, true)returns the assoc array; the_json_decode_assocruntime flag threads the choice through nested objects.json_encode()ofstdClasswalks the dynamic-property hash through a stdClass-aware wrapper that preserves{}for empty instances. - Structural decode-error detectors.
json_decode()uses a checked recursive decoder instead of a full-buffer pre-validation pass: invalid input returnsMixed(null)and setsJSON_ERROR_SYNTAX(and raisesJsonExceptionunderJSON_THROW_ON_ERROR); depth overflow setsJSON_ERROR_DEPTHand raisesJsonExceptionlikewise. The_json_active_flags/_json_depth_limit/_json_active_depthplumbing is shared withjson_validate()andjson_encode().json_last_error()/json_last_error_msg()reflect the failure;json_decode()resets the slot at entry so a previous failure does not leak. -
preg_match(),preg_match_all(),preg_replace(),preg_split() -
exec(),shell_exec(),system(),passthru() -
getenv(),putenv() -
php_uname(),phpversion() - Constants:
PHP_EOL,PHP_OS,DIRECTORY_SEPARATOR
- Free-list allocator (replace bump allocator with reusable memory)
- Heap allocation headers (8-byte block size, minimum 8-byte allocation)
-
__rt_heap_free/__rt_heap_free_safe— return blocks to free list - Copy-on-store (
__rt_str_persist) — strings persisted to heap, concat buffer is scratch-only - Concat buffer recycling (reset per statement, no more overflow)
- Free on reassignment (old string/array freed when variable is overwritten)
-
unset()frees heap memory - Configurable heap size (
--heap-size=BYTES, default 8MB) - Heap bounds checking with fatal error message
- Array push capacity checking with fatal error message
-
include/require/include_once/require_once - Dynamic array growth (automatic 2x reallocation on push beyond capacity)
- Dynamic hash table growth (automatic 2x rehash at 75% load factor)
- Persist strings to heap before pushing to arrays and hash values
- String deduplication —
str_persistskips copy for .data and heap strings (only copies from concat_buf) - Block coalescing — bump pointer reset when freeing the last allocated block (O(1), zero fragmentation for
.=loops) - Deep free for arrays via
unset()(frees string elements + array struct) - Zero-init local variables in function prologues (prevents stale pointer frees)
- Classes with
public/privateproperties and optional defaults - Constructor (
__construct) with arguments - Instance methods with
$thisaccess - Static methods via
ClassName::method() - Static properties via
ClassName::$prop,self::$prop,parent::$prop, andstatic::$prop -
::classmagic constant (Class::class,self::class,parent::class,static::class) -
new self()/new static()/new parent()factory pattern -
newkeyword for object instantiation -
->property access and method calls - Nullsafe property access and method calls with
?->for nullable object receivers -
readonlyproperties (enforced at compile time) - Property type declarations (
public int $x,readonly ?string $name) with checked defaults and assignments - Objects as function parameters and return values
- Objects stored in arrays
- Reference counting infrastructure (header:
[size:4][refcount:4], zero overhead) - Runtime:
__rt_incref,__rt_decref_array,__rt_decref_hash,__rt_decref_object -
unset()uses decref (frees when refcount drops to zero) - GC statistics (
--gc-statsflag: allocations, frees printed to stderr) - Strings freed on variable reassignment (value-copied, always owned)
- Ordinary local/global reassignment now releases previous arrays/objects safely, and indexed array writes / associative-array writes / object property writes /
staticslots now retain borrowed heap values consistently - Automatic epilogue cleanup has since been re-enabled for locals proven to own heap values; the remaining gaps are conservative control-flow merges and cyclic graphs
- Assoc-derived and broader container-copy paths now retain borrowed heap values consistently; the main remaining memory-model work has moved to targeted cycle collection, richer debug instrumentation, and tighter ownership precision
-
sin(),cos(),tan() -
asin(),acos(),atan(),atan2() -
deg2rad(),rad2deg() -
sinh(),cosh(),tanh()
-
log()— natural logarithm -
log2(),log10() -
exp()— e^x
-
hypot()— sqrt(x² + y²) -
pi()— alias for M_PI
-
M_E,M_SQRT2,M_PI_2,M_PI_4,M_LOG2E,M_LOG10E -
PHP_FLOAT_MIN,PHP_FLOAT_EPSILON
- Opaque pointer type (
ptr) for handles andvoid* - Typed pointer tags via
ptr_cast<T>()for annotating raw addresses with a checked pointee type - Pointer builtins:
ptr(),ptr_null(),ptr_is_null(),ptr_offset(),ptr_cast<T>(),ptr_get(),ptr_set() - Raw buffer pointer builtins:
ptr_read8(),ptr_read32(),ptr_write8(),ptr_write32() -
ptr_sizeof()— returns byte size of a type ("int"→ 8,"float"→ 8, class name → computed) - Pointer echo:
echo $ptrprints hex address (0x...) - Pointer comparison:
===,!==between pointer values
-
extern functiondeclarations with C type annotations (int,float,string,bool,void,ptr) -
extern "libname" { }blocks (auto-llinker flag) -
extern "libname" function name(): type;single-line syntax -
--link/-land--link-path/-LCLI flags -
--frameworkflag for macOS frameworks - Owned null-terminated string ↔ length-prefixed string conversion (
__rt_str_to_cstr,__rt_cstr_to_str) -
extern classfor C struct mapping (flat layout, available toptr_sizeof()and typed pointer field access) -
extern globalfor accessing C global variables - Callback support: pass elephc functions as C function pointers (
callableparams) - C memory management via extern libc:
malloc(),free(),memcpy(),memset() - Native interop validation examples: raw FFI memory + SDL2 window/input/framebuffer/audio demos
- Ownership lattice for heap values in codegen (
Owned/Borrowed/MaybeOwned/ non-heap) - Re-enable epilogue cleanup for locals that are proven to own their heap values
- Broader container propagation rules for nested array/hash/object transfers
- Focused regressions for aliasing across locals, returns, nested containers, and scope exit
- Heap allocator improvements: adjacent-block coalescing and less fragmentation under mixed allocation sizes
- Runtime heap verification / debug mode (
double free, bad refcount, free-list corruption checks) - Uniform runtime heap-kind metadata for arrays / assoc arrays / objects / persisted strings
- Evaluate a cycle-collection strategy for circular container/object graphs
- Introduce targeted cycle collection for circular array/hash/object graphs
- Add uniform
__rt_decref_any/ heap-kind-based release dispatch for mixed heap values - Emit richer runtime metadata for refcounted object/container payload scanning
- Extend
--heap-debugwith leak summaries, high-watermark stats, and freed-block poisoning - Introduce segregated free lists / size classes to reduce allocator scan cost and fragmentation
- Tighten ownership propagation in remaining conservative control-flow / merge paths
- Formalize FFI heap ownership boundaries for borrowed vs owned native buffers and strings
- Copy-on-write arrays — PHP-style shared-until-modified semantics with a COW flag in array headers and copy-on-mutation
- Inheritance (
extends) — vtable-based method dispatch, property layout chaining, andself::/parent::/static::calls - Interfaces / abstract classes — interface method tables and compile-time conformance checking
-
instanceof— class/interface runtime metadata checks with inheritance, interface inheritance,self,parent, and late-boundstatic - Traits — compile-time method copying / inlining with
use,as,insteadof, and trait properties - Exceptions (
try/catch) — stack unwinding viasetjmp/longjmpwith runtime frame cleanup andfinallysupport - Hash table insertion order — preserve PHP associative-array insertion order with a secondary linked list through entries
- Mixed-type associative arrays — per-entry type tags instead of one value type per table
- String indexing (
$str[$i]) — lower to one-character slice syntax as sugar for string reads -
protectedvisibility — third visibility level between public and private - Magic methods (
__toString,__get,__set) — implicit hooks on property access and string conversion - ifdef or similar support
- Hot-path data type
- Full namespace support
- Comprehensive error recovery (multiple errors per compilation)
- Warning system (unused variables, unreachable code)
- Enums (
enum Color { Red; Green; Blue; }) — backed enums with->value,::from(),::cases() - Named arguments (
foo(name: "Alice", age: 30)) — reorder args at compile time based on parameter names - First-class callable syntax (
strlen(...)) — create closures from function names without string indirection -
matchwith no-match error — runtime fatal when no arm matches and no default - Readonly classes (
readonly class Point {}) — all properties implicitly readonly - Final classes, methods, and properties (
final class Foo {},final public function run() {},final public $id) — compile-time inheritance and override enforcement - Union types (
int|string) — tagged union with runtime type dispatch - Nullable types (
?int) — sugar forint|null - Function / method parameter and return type hints (
function foo(int $x): string) — compile-time validation for functions, methods, constructor parameters, closures, arrow functions, and non-voidreturn-path coverage - Constructor property promotion (
public function __construct(public int $x)) — promoted parameters lower to declared properties plus constructor assignments, including visibility,readonly, defaults, nullable/union type declarations, and by-reference promoted parameters
- Linux x86_64 target
- Linux ARM64 target
- Split
src/codegen/expr.rsinto a slim dispatcher plus smaller focused helpers - Split
src/codegen/stmt.rsinto a slim dispatcher plus smaller focused helpers - Target support matrix:
macos-aarch64,linux-aarch64,linux-x86_64
- Runtime object cache — pre-assemble the runtime into
~/.cache/elephc/runtime-<version>-<runtime-hash>.oand reuse across compilations, invalidating on compiler version, target, heap size, or generated runtime assembly changes. Cuts repeated compile time by ~50%. - Benchmark suite (vs C, vs PHP interpreter)
- Source maps (assembly ↔ PHP line mapping)
- Compiler timing / profiling output for parse, typecheck, codegen, assemble, and link phases
- Benchmark automation — run the benchmark harness in CI, publish markdown summaries and JSON artifacts, and use it as a correctness/trend gate without noisy hard thresholds
- Constant folding (
2 + 3→5at compile time) - Dead code elimination
- Add regression benchmarks so optimization work is measured instead of anecdotal
- Constant propagation across locals / statement boundaries
- Path-aware dead code elimination foundations — shared reachability/tail-path analysis for
if/ifdef/switch/try, shadowed handler/pattern removal, and guard-aware nested region pruning - Purity / may-throw analysis so AST optimizations can reason more precisely about safe hoisting and branch removal
- Exception-aware dead code elimination beyond conservative
try/catch/finallyheuristics — catch/finally guard invalidation now tracks pre-handler throw paths, including CFG-pruned switch paths - Control-flow normalization pass for flattening redundant nested
if/switch/tryshells after pruning - Alias-aware constant propagation so local callables and scalar values can stay precise across
if/switch/trymerges - Relational and loose-comparison contradiction guards for dead-code elimination
- Advanced static property parity — PHP-style static property redeclaration rules and direct array element writes such as
ClassName::$items[] = $value - Short ternary operator
?:— PHP Elvis form with single evaluation of the left-hand expression -
printexpression form — writes output and returns1, including statement-formprint $x;asExprStmt(Print(...)) - Constant propagation v2 — known-subject
switchpath merges, non-throwingtry/ unreachable-catch env merges, knownmatchfolding, and scalar indexed/associative array-literal access folding - Constant propagation v3 — local loop path summaries for
while(false),do...while(false),while(true)/for(;;)break exits, branch-local loop-exit merges, and safe pruning arounddo...while(false)loop exits - PHP-compatible magic constants:
__DIR__,__FILE__,__LINE__,__FUNCTION__,__CLASS__,__METHOD__,__NAMESPACE__,__TRAIT__(case-insensitive names, per-file include scope, closure names, trait__CLASS__rebinding) - Compile-time-constant expressions in
include/requirepaths (string literals, concat, magic constants, namespace-awareconst/use const/define()refs) - Error-control operator
@backed by a suppressible runtime warning channel and exception-safe suppression-depth restoration - Runtime-value compatibility pass for
strpos()/strrpos()/array_search()/file_get_contents()false-return conventions anddefine()boolean duplicate behavior - Static closures:
static function() { }andstatic fn() => ...(no$thiscapture) - PHP array union operator
+for indexed+indexed and associative+associative arrays, preserving left-side duplicate keys and associative insertion order - PHP-compatible associative array key normalization for integer keys and numeric-string keys across literals, reads/writes,
foreach,array_keys(),array_search(),array_key_exists(),array_flip(),json_encode(), and associative array union - PHP-compatible octal integer literals: legacy leading-zero octal (
0755,0_755) and PHP 8.1 explicit octal (0o755/0O755) alongside the existing decimal and hexadecimal forms - PHP 5.4 binary integer literals (
0b1010/0B1010) - PHP 7.4 numeric separators (
1_000_000,0xFF_FF,0b1010_1010,0o7_7_7,1_000.5,1e1_0) across decimal, hex, octal, binary, and float literals - Trailing-character validation on numeric literals (rejects
0o78,078,0xfg,0b12,1_,1__0at lex time instead of silently splitting tokens) - Support for
neverreturn type -
iterablepseudo-type runtime parity —foreachover indexed-array, hash-backed,Iterator, andIteratorAggregateiterables;echo,gettype(),var_dump(),===, scalar casts ((int),(float),(string),(bool)), and theis_iterable()builtin all dispatch through heap-kind, value-type, or interface metadata where needed - Filesystem modification:
touch()(with optional nullable$mtime/$atime, explicit numeric timestamps including-1, and PHP-style0666 & umaskcreation mode),chmod(),chown()/chgrp()with numeric IDs or string names,umask()(with the no-arg probe form),ftruncate(),fflush()(implemented asfsync()),fsync(), andfdatasync()(with a Darwinfsyncfallback). Runtime paths use libc where practical, with target-aware file-creation handling fortouch(). - Filesystem path manipulation:
basename(),dirname()including multi-level parent lookup,pathinfo()(component flags returning strings plus no-flag /PATHINFO_ALLassociative-array forms),realpath(),fnmatch()with shell-glob*/?/[...]/\\support and zero flags, plus thePATHINFO_DIRNAME/PATHINFO_BASENAME/PATHINFO_EXTENSION/PATHINFO_FILENAME/PATHINFO_ALLconstants - Filesystem metadata coverage: scalar getters (
fileatime(),filectime(),fileperms(),fileowner(),filegroup(),fileinode(),filetype()) with PHP-compatiblefalseon stat failure, permission predicates (is_executable(),is_link(),is_writeable()alias ofis_writable()), full PHP-compatiblestat()/lstat()/fstat()arrays (numeric0..=12+ string keysdev/ino/mode/nlink/uid/gid/rdev/size/atime/mtime/ctime/blksize/blocks) with PHP-compatiblefalseon failure, andclearstatcache()as a no-op (elephc has no stat cache)
Close the small, well-scoped PHP filesystem/runtime compatibility gaps that are already adjacent to the current implementation, plus the language-level PHP behavior visible in everyday source code that does not require a new backend or product mode. This is also the series that delivered the Fibers MVP.
-
fnmatch()non-zero flag parity forFNM_PATHNAME,FNM_PERIOD,FNM_CASEFOLD, andFNM_NOESCAPE - Dynamic
pathinfo($path, $flag)parity for runtime flags that may evaluate toPATHINFO_ALL; runtime exactPATHINFO_ALLnow returns the associative-array shape while component flags return strings - PHP resource type compatibility — model file handles and future extension handles separately from integers
-
fopen()failure parity — returnfalseon open failure while keeping successful handles asresource, and make stream built-ins reject/handle theresource|falsepath without passing boxedfalseas a native descriptor - Runtime-dynamic include paths — explicitly reject runtime-evaluated
include/requirepath expressions beyond the current compile-time string-folder ($path, function calls, ternaries, property access) - Runtime-order-aware
include_once/require_once— add runtime guards inside functions, methods, loops, and conditional branches so skipped files match PHP execution order rather than only compile-time traversal order - Include graph declaration discovery — pre-scan all statically resolvable
include/requiretargets for function/class/interface/trait declarations before name resolution and type checking, so symbol references are not sensitive to source include order while top-level include execution order remains PHP-compatible - Path-sensitive include declaration discovery — avoid false duplicate declaration errors when the same statically resolvable regular
include/requiretarget is reachable only through mutually exclusive control-flow paths, while still reporting duplicates for sequential loads and potentially repeated loop loads - Runtime-loaded include function dispatch — compile include-discovered functions behind public dispatchers activated at each real include point, so direct calls and
function_exists()follow PHP runtime load order while type checking can still see the include graph - Conditional include function variants — when mutually exclusive
if/elseif/elsebranches include different files that declare the same function name with identical signatures, compile each declaration as a hidden variant and dispatch the public function name to the variant loaded at runtime - Mixed nullsafe/member chains — match PHP's full chain semantics for forms that mix
?->and->, such as$a?->b->c - Dynamic
instanceoftargets — support PHP forms such as$obj instanceof $classNamewith runtime validation for class-string/object target expressions - Full PHP list destructuring — skipped entries, nested patterns, associative-key destructuring, and non-local destructuring targets where PHP permits them
- Named-argument parity for built-ins, extern calls, and spread — extend validation/lowering outside user-defined calls and handle spread interactions
- Fibers MVP —
FiberandFiberErrorbuilt-in classes,start()/resume()/suspend()/throw()/getReturn(), state predicates,Fiber::getCurrent(), closure captures, uncaught-exception propagation through the caller, guarded per-fibermmapstacks, and context switching on ARM64 plus Linux x86_64 - Full first-class callable targets — support
static::method(...)and$object->method(...)in addition to function,ClassName::,self::, andparent::targets - Captured closures as callback values — forward hidden
use (...)environments through callback-style built-ins such asarray_map,array_filter, andcall_user_func
-
JSON_BIGINT_AS_STRINGinjson_decode(): integer-grammar tokens (no., noe/E) whose magnitude exceeds PHP_INT_MAX (9223372036854775807) promote to aMixed(string)preserving the original digits; in-range integers and float-grammar tokens are unaffected. Length-then-lex compare against the threshold strings9223372036854775807(positive) /-9223372036854775808(negative) detects overflow without invoking__rt_atoi(which silently wraps viaimul). - Lone UTF-16 surrogate detection in
json_decode()/json_validate(): every\uXXXXescape in the high-surrogate range (0xD800..0xDBFF) must be immediately followed by a\uYYYYescape in the low-surrogate range (0xDC00..0xDFFF). Unpaired high surrogates and stand-alone low surrogates setJSON_ERROR_UTF16(10) and raiseJsonExceptionunderJSON_THROW_ON_ERROR, with the PHP-faithful messageSingle unpaired UTF-16 surrogate in unicode escape. The string parser accumulates each escape into a 16-bit codepoint and walks the surrogate-pair handshake before resuming content scanning. - PHP-strict depth semantics for
json_decode()/json_validate(): PHP rejects when the active nesting depth equals the$depthargument (active >= limit), so a flat array fails at depth=1 and a one-level-nested container fails at depth=2. The shared__rt_json_depth_enterkeeps the lenientactive <= limitrule (used byjson_encode()per PHP), and the decode/validate dispatchers pass$depth - 1as the runtime limit so both surfaces match PHP exactly without forking the depth helper. -
Exception::$codeandException::getCode(): int. The constructor now accepts an optional$code = 0second argument and stores it as aprotected intproperty.JsonExceptionthrown viaJSON_THROW_ON_ERRORcarries the originatingJSON_ERROR_*code, socatch (JsonException $e) { $e->getCode(); }matches PHP exactly (4 = SYNTAX, 1 = DEPTH, 10 = UTF16, 7 = INF_OR_NAN, etc.). User-codenew Exception("msg", $code)also surfaces the value throughgetCode(). -
is_callable($value): boolbuiltin. Compile-time decision when the value is a string literal (resolves against the catalog + user functions; case-insensitive for builtins) or aCallable-typed expression (closures, first-class callables). Non-literal strings,[$obj, "method"]arrays, and__invokeobjects route to a future runtime helper. - Cache
_json_active_flagsin a callee-saved register (x19ARM64 /r15x86_64) inside__rt_json_encode_str: 8 reload sites collapse to a single-instructiontst/testagainst the cached register, eliminating one address-load + memory dereference per HEX_/UNESCAPED_/UTF-8 dispatch in the per-byte escape loop.
Close broad PHP-visible parity gaps across the value model, modern syntax, runtime helpers, and standard-library surfaces.
- Heterogeneous indexed arrays — allow mixed payloads in indexed arrays instead of requiring homogeneous indexed values
- PHP 8.0 attribute syntax —
#[Name],#[Name(args)], stacked groups (#[A] #[B]), comma-separated within a group (#[A, B(1)]), qualified names (#[\Ns\Name]). Lexer addsToken::AttrOpenfor#[; bare#becomes a PHP-style line comment (no longer ambiguous). Parser invokesparse_attribute_listsat every site PHP allows: top-level statements, class/trait/interface members, enum cases, function/method parameters, closures, arrow functions. Attributes preserved in the AST viaattributes: Vec<AttributeGroup>onStmt,ClassProperty,ClassMethod,EnumCaseDecl, plus per-ClassConststorage on the newClassConstAST type. Class-like declarations gainconstants: Vec<ClassConst>. -
#[\Override]enforcement — methods marked#[\Override]must override a parent-chain method; otherwise the type checker emits the PHP-faithful error"<Class>::<method>() has #[\Override] attribute, but no matching parent method was found". -
#[\Deprecated]warning — calls to functions/methods marked deprecated emit"Call to deprecated function: <name>()"warnings, optionally appending the user-supplied reason. Reason extraction lives intypes::checker::schema::validation::extract_deprecationand threads throughFunctionSig::deprecation. - User-defined
#[Attribute]declarations — classes marked#[Attribute]parse without error and accept argument lists at usage sites (#[MyAttribute("test")] class C {}). - Generators /
yieldMVP —Generatorbuilt-in class withyield,yield $k => $v, generator functions and captured generator closures,$x = yieldresume assignment, boxedGenerator::send()payload delivery,Generator::throw(),Generator::getReturn(), terminalreturn <expr>, state-machine codegen backed by heap-allocatedGeneratorFrameobjects on ARM64 and Linux x86_64, and yield-context validation that rejectsyieldoutside functions or insidetry/catch/finally -
yield fromdelegation — forward iteration through compile-time array literals, direct generator calls, and local generator variables, including case-insensitivefromparsing and cleanup of owned direct-call delegates after completion - Filesystem stream extensions:
fgetc()(thin wrapper overfread, length 1, returningfalseat EOF/read failure),readfile()(open + chunked read+write to stdout + close, returns bytes copied,-1on read failure, orfalseon open failure),fpassthru()(same loop on an already-open fd, returning-1on read failure),flock()(libcflockwith PHP→POSIXLOCK_UNtranslation, preserves theLOCK_NBflag, and supports the optional$would_blockoutput), andtmpfile()(mkstemp("/tmp/elephc-XXXXXX")+ immediateunlinkso the file auto-deletes on close, returns a PHPresource|false). Also predefinesLOCK_SH=1,LOCK_EX=2,LOCK_UN=3,LOCK_NB=4constants matching PHP's numbering. - Filesystem symbolic links:
symlink($target, $link),link($target, $link),readlink($path)(returns owned heap string boxed as Mixed for thestring|falseconvention), andlinkinfo($path)(returnsst_devor PHP's-1failure sentinel). All routed through libc to avoid per-syscall remapping work. - PHP 8.5 pipe operator (
|>) — left-associative, lower precedence than additive operators, supporting first-class callables, static and instance methods, closures, and variable callables; rejects by-reference callable parameters - PHP attributes runtime introspection — implement
ReflectionClass::getAttributes(),ReflectionMethod::getAttributes(),ReflectionProperty::getAttributes(), plusReflectionAttribute::newInstance(). Class/member declarations expose attribute names and supported literal args through helper builtins and Reflection objects;ReflectionAttribute::newInstance()constructs the attribute class on demand from the captured literal args. - Mixed indexed/associative array union — model
array + arrayacross indexed/hash representations while preserving PHP's shared int/string key space and left-key precedence - Callable parity follow-up — support captured method/static first-class callables in the remaining callback runtimes (
array_reduce(),array_walk(),usort(),uksort(),uasort()), direct callable expression calls such as($obj->method(...))(), non-local method receivers such as(new Foo())->method(...), nullsafe first-class callables, broader builtin first-class callable wrappers, and the remainingcall_user_func_array()by-reference callback gaps - Runtime-value compatibility polishing v2 — uninitialized typed instance/static property reads fail with PHP-style fatal diagnostics; constant-folded and non-folded runtime integer
+/-/*overflow promotes to double; scalar loose comparisons cover PHP bool truthiness, null-vs-empty-string, numeric-string, and non-numeric string byte-comparison rules at constant-fold and runtime helper sites. Warning/notice sites added so far route through the suppressible runtime diagnostics channel. - Broader date and regex PHP parity — expand
strtotime()relative formats witha/an <unit>article offsets, addpreg_replace()capture backreference expansion ($0..$9,\0..\9), and move preg runtimes to PCRE2-backed matching (JSON parity now closed: see v0.8.x base + v0.20.x polish) - JSON encoder optimization — folded
__rt_json_assoc_is_list_shapeinto the main associative-array encoding walk.__rt_json_encode_assocnow emits a provisional object form, tracks whether keys remain0..count-1while iterating the hash once, and compacts the finished buffer in-place to[...]only for real list-shape payloads. Object-shape inputs still stay object form, andJSON_FORCE_OBJECTdisables compaction. - JSON decoder optimization — fused the
__rt_json_validatepre-pass into__rt_json_decode_mixedforjson_decode(). The wrapper now calls the checked structural decoder directly; the decoder trims the input once, validates scalar strings/numbers at the point where they are decoded, enforces depth around containers, records syntax/depth/UTF-16 errors internally, and returns null-on-error for the PHP-facing wrapper.json_validate()keeps the standalone RFC 8259 validator surface. - JSON encoder optimization — extended the
_json_active_flagscallee-saved-register cache to__rt_json_encode_assocand__rt_json_encode_array_dynamic(x19ARM64 /r15x86_64). The recursive encoder chain now preserves that cache:__rt_json_encode_objectno longer clobbers ARM64x19, and the x86_64 string encoder keepsr15dedicated to cached flags during UTF-8 decoding. - JSON pretty-print optimization — inline indent emission inside each container encoder (assoc, array_int/str/dynamic, object) and retire the
__rt_json_pretty_applypost-processor. Eliminates the second buffer walk for JSON_PRETTY_PRINT workloads. Multi-day refactor completed with a_json_indent_depthBSS slot, balanced normal-path formatting depth maintenance, reset-at-entry protection across throws, and bytewise PHP cross-check coverage on representative payloads. -
is_callable()runtime fallback — handle non-literal strings,[$obj, "method"]arrays, and objects implementing__invoke. The string-literal + Callable-typed compile-time path is already in place. - Case-insensitive user-function lookup —
function_exists("USER_FN")andis_callable("USER_FN")accept any case for user functions through a shared lookup path, matching PHP's function-name rules. - OOP property parity v2 — PHP 8.4 property-hook contracts now cover interface properties and abstract properties in traits/classes;
readonly staticremains rejected like PHP, instance property redeclaration validates hook get/set contracts, and by-reference constructor promotion now rejects readonly aliases at compile time while supporting default-value reference cells.
PHP-compatible Standard PHP Library coverage, rolled out in phases.
- Phase 1 — built-in interfaces:
Traversable,Iterator(extendsTraversable),IteratorAggregate(extendsTraversable),OuterIterator,RecursiveIterator,SeekableIterator,Countable,ArrayAccess,SplObserver,SplSubject,Stringable,JsonSerializable - Phase 2 —
count($obj)redirects toCountable::count() - Phase 3 — SPL exception hierarchy:
LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,RuntimeException,OutOfBoundsException,OverflowException,RangeException,UnderflowException,UnexpectedValueException - Static autoload — composer.json
autoload.psr-4driven, includesvendor/<vendor>/<package>/composer.json. Replaces runtime autoload by inlining every reachable PSR-4 class at compile time -
spl_autoload_*stubs —register/unregisterreturntrue,functionsreturns[],extensionsreturns".inc,.php",callandspl_autoloadare no-ops. Defensive code that calls these at boot compiles unchanged - Closure-aware
spl_autoload_register— the closure body is evaluated symbolically at compile time. Supports__DIR__ . '/' . str_replace('\\', '/', $name) . '.php'style autoloaders, intermediate variable assignments, andif (file_exists(...))guards.spl_autoload_unregisterremoves matching rules;spl_autoload_call("App\\Foo")with a literal name forces compile-time autoload of that class - Runtime r/w for
spl_autoload_extensions— backed by mutable globals (_spl_autoload_exts_ptr/_spl_autoload_exts_len) initialized to".inc,.php". Read returns the current value; write swaps in the new and returns the previous, matching PHP semantics -
spl_autoload_functions()returns an indexed array sized to the number of registered closure rules —count()andforeachsee one entry per rule - Read
autoload.classmap,autoload.files,autoload.psr-0, andautoload-dev.*sections from composer.json. Longest-prefix wins for PSR-4. PSR-0 supports both namespaced and underscore-class conventions -
class_exists/interface_exists/trait_exists/enum_existswith literal class name andautoload = true(default) trigger compile-time autoload of the literal - Variable-stored closures and function-name string callables are accepted by
spl_autoload_register. The closure assignment / function declaration is stripped from the program after the rule is extracted - Top-level
if (...)whose condition folds to a literal bool flattens before rule collection — guard your register call withif (true),if (false)/else, or chainedelseifand the chosen branch is inlined at compile time -
sprintf,dirname,basenameare supported by the symbolic interpreter — common autoloader patterns likerequire_once sprintf("%s/%s.php", __DIR__, $name)anddirname(__DIR__) . '/lib/' . $name . '.php'fold at compile time -
get_declared_classes/get_declared_interfaces/get_declared_traitsreturn AOT introspection snapshots of the compiled symbol set -
class_alias($orig, $alias)synthesises a subclass at compile time sonew $alias()andinstanceof $aliaswork as the user expects -
autoload.exclude-from-classmapskips matching paths during classmap scanning. Supports glob patterns (*,**,?) plus the trailing-slash directory shorthand - PSR-4 empty namespace prefix
""(root namespace) verified working -
realpathandpathinfo(withPATHINFO_*flags) added to the symbolic interpreter - PSR-0 underscore-class convention (
Twig_Loader_Filesystem→lib/Twig/Loader/Filesystem.php) verified -
spl_object_id,spl_object_hash,spl_classesruntime helpers; pointer-based identity, stable per process -
get_class/get_parent_classresolve via the argument's static type at compile time -
is_a/is_subclass_ofwith literal class arg fold at compile time, walking parent chain and implemented interfaces - Compile-time warning when a
spl_autoload_registerclosure is rejected (use captures, multi-param, variadic) — explains why the autoloader silently became a no-op
Serializable is intentionally not implemented — it has been deprecated since PHP 8.1.
Close the small dispatch, lvalue, and runtime correctness gaps that should not be carried through a backend migration.
-
Throwable-via-interfacegetMessage()dispatch fix (pre-existing): catching byThrowableand callinggetMessage()on the typed binding returns garbage instead of the message string -
$obj[$k]subscript syntax forArrayAccessimplementers (read, write,isset,unsetpaths), includingMixed-boxing for offsets and values - Raw pointer memory helpers for FFI/socket showcases:
ptr_read16(),ptr_write16(),ptr_read_string(),ptr_write_string() -
IntrinsicCallfoundation for runtime-managed SPL/core objects when direct method interception is still the cleanest implementation path
Continue SPL coverage on the 0.x path, but do not let broad library coverage block the EIR migration unless it exposes core dispatch, ownership, or lvalue gaps that must be fixed first.
- Phase 4 —
SplDoublyLinkedList,SplStack,SplQueue,SplFixedArray - Runtime callable dispatch metadata foundation — shared AOT callable cases for entry-selected callbacks and runtime string-name user callbacks, reused by
call_user_func(),call_user_func_array(), anditerator_apply() - Phase 5 storage foundation —
EmptyIterator,ArrayIterator,ArrayObject - Phase 5 simple iterator decorators —
IteratorIterator,LimitIterator,NoRewindIterator,InfiniteIterator - Phase 5 multi-source iterator decorators —
AppendIterator,MultipleIterator - Phase 5 filter/cache decorators —
FilterIterator,CallbackFilterIterator,CachingIterator - Phase 5 recursive iterator family —
RecursiveArrayIterator,RecursiveFilterIterator,RecursiveCallbackFilterIterator,RecursiveIteratorIterator,ParentIterator - Phase 5 — iterator decorators (
ArrayIterator,ArrayObject,IteratorIterator,LimitIterator,NoRewindIterator,InfiniteIterator,EmptyIterator,AppendIterator,MultipleIterator,CallbackFilterIterator,FilterIterator,CachingIterator,RecursiveArrayIterator,RecursiveCallbackFilterIterator,RecursiveFilterIterator,RecursiveIteratorIterator,ParentIterator); functionsiterator_to_array,iterator_count,iterator_apply,class_implements,class_parents,class_uses - Runtime callable dispatch expansion — generated descriptor cases for dynamic string builtin callbacks and public
Class::methodstrings, pluscall_user_func()/call_user_func_array()support for invokable objects and callable arrays stored directly or in local variables - Runtime callable descriptor ABI/storage foundation — closure, first-class callable, SPL callback-adapter, object-property, array, local, and Fiber storage now carries descriptor pointers; indirect call sites and callback runtimes load the entry ABI slot before invocation
- Universal runtime callable descriptors — complete runtime descriptor metadata for signature/default/by-ref/variadic handling, receiver/capture environments, and invocation support for string, array, closure, first-class callable, object
__invoke, static/instance method, builtin, and extern callable shapes - Phase 5 follow-up — iterator-dependent Phase 4 parity:
SplFixedArray::getIterator()plusIteratorAggregate/InternalIteratorruntime wiring once iterator classes are available - Phase 6 —
SplHeap,SplMaxHeap,SplMinHeap,SplPriorityQueue,SplObjectStorage, and per-instance handle finalization - Phase 7 —
RegexIterator,RecursiveRegexIterator - Phase 8 — file/directory iterators:
SplFileInfo,SplFileObject,SplTempFileObject,DirectoryIterator,FilesystemIterator,GlobIterator,RecursiveDirectoryIterator,RecursiveCachingIterator - Object destructors (
__destruct) — invoked when an object's refcount reaches zero (scope exit, reassignment,unset, program end), before its properties are released. Dispatched by runtime class_id through a_class_destruct_ptrstable (__rt_call_object_destructorat the top of__rt_object_free_deep, both targets); inherited destructors resolve to the implementing ancestor's method; a refcount-word guard stops a self-referencing body from re-entering the free path. Object resurrection is intentionally unsupported. Validated as a magic method (non-static, zero-arg)
End-to-end PHP streams/sockets/network subsystem, rolled out in
phases on feat/streams-sockets. Each phase landed as an autonomous
increment with its own tests; the descriptions below summarize the
PHP-visible surface, not the internal commits.
- Phase 1 —
is_resource/get_resource_type/get_resource_idintrospection,STREAM_*/PSFS_*/FILE_*/GLOB_*constants,stream_isatty/stream_is_local/stream_supports_lock/stream_get_transports/stream_get_wrappers/stream_get_filtersstubs - Phase 2 —
stream_context_*resources,php://memory/temp/stdin/stdout/stderranddata://pseudo-wrappers,stream_get_contents/stream_copy_to_stream - Phase 3 — filter chain (
stream_filter_append/prepend/remove) withstring.toupper/tolower/rot13/zlib.deflate/zlib.inflatebuilt-ins - Phase 4a — TCP socket family (
stream_socket_client/server/accept) and thehttp://wrapper - Phase 4b —
elephc-tlsstaticlib bridge (rustls) and thehttps://wrapper, linked indirectly so non-https programs stay libc-only - Phase 5 — UDP and Unix-domain sockets (
udp://,unix://,udg://),stream_socket_sendto/recvfrom,stream_socket_pair,stream_socket_get_name - Phase 6 —
ftp://wrapper,opendir/readdir/closedir/rewinddirwith theglob://directory wrapper - Phase 7 — memory streams (
php://memory,php://temp),stream_get_line,disk_free_space/disk_total_space - Phase 8 — stream options (
stream_set_blocking/set_timeout/get_meta_data, plus the chunk/read/write buffer stubs) - Phase 9 — network utilities (
gethostby*,ip2long/long2ip,inet_pton/ntop,getservby*,gethostname),popen/pclose,fsockopenwith by-reference error outputs,stream_select - Phase 10 — user-defined wrappers:
stream_wrapper_register("scheme", "Class")plusnew $variable()parser/runtime sofopen("scheme://...")instantiatesClassand dispatchesstream_open/stream_read/stream_write/stream_close/stream_eof/stream_seekthrough the regular method ABI. fopen returns synthetic descriptors in the0x40000000+range;fread/fwrite/fclose/feof/fseekdetect those descriptors and tail-call dedicated wrapper helpers instead of touching the libc-fd-indexed runtime tables - Phase 11 — maximal-parity push (rebased onto current
origin/main): feof-first whole-stream drains sostream_get_contents/fpassthru/fgets/stream_copy_to_streamwork on userspace-wrapper descriptors without corrupting the caller's resource cell;fgetc/rewindwrapper dispatch;php://filter/[read=|write=]F/resource=…wrapper;fprintf/fscanf;convert.iconv.<from>/<to>charset filter via libciconv(macOS auto-links-liconv);ssl.cafilecustom CA bundle (plus the existingssl.verify_peer=0) forhttps://; socket context options includingsocket.so_broadcast;pfsockopen. Also previously:compress.bzip2://andftps://(RFC 4217) wrappers,tcp_nodelay/so_reuseport/bindto/ipv6_v6onlysocket options - Phase 11 follow-up — pre-existing-bug fixes surfaced by the streams work: bzip2 x86_64 frame alignment (qemu SIGSEGV),
sscanf/fscanf%f, type-aware out-of-bounds array-read null fallback, and string-search builtins (strpos/str_contains/str_starts_with/str_ends_with/strstr/strrpos) coercing Mixed/string\|falseoperands on x86_64 - Phase 11 follow-up —
bzip2.compress/bzip2.decompressstream filters (libbz2, indirect-fn-pointer pattern; interoperable with PHP'sbzcompress/bzdecompress) - Phase 11 follow-up — write-direction
convert.iconv.*filter (STREAM_FILTER_WRITEtranscodes eachfwritevia libciconvthrough the same indirect-fn-pointer write-filter mechanism; id 12) - Phase 11 follow-up —
socket.backlogcontext option (__rt_socket_backlogreads['socket']['backlog']and feeds the TCP/IPv6/Unixlisten()backlog; default 128) andftp.resume_pos(REST <N>beforeRETR, shipped earlier) - Phase 12 — userspace-wrapper coverage completion: property-default init on dynamic
new $var()/ registered wrappers + filters (per-class_class_propinit_<id>thunk run by__rt_new_by_name);fstat()viastream_stat; path-basedfile_exists()/filesize()/is_file()viaurl_stat(string $path, int $flags)(vtable 8→10, a__rt_path_is_wrapperscheme matcher, and a shared__rt_box_wrapper_stat_result);readfile()on wrapper URLs (fopen + feof-gated drain + close);fgetcsv()andstream_get_line()on wrapper handles (runtime__rt_fgets/__rt_stream_get_linegained a feof-gated__rt_freadloop accumulating into_user_wrapper_drain_buf). Stat methods must be declared without a return type so the assoc stat array round-trips as a boxed Mixed - Phase 13 — TLS stream-context ssl options:
ssl.capath(a directory of PEM CA certs → trust anchors, viaelephc_tls_connect_capath),ssl.peer_name(verify the cert / send SNI for a name other than the connection host, viaelephc_tls_connect_peer_name), andssl.allow_self_signed/ssl.verify_peer_name = "0"(relaxed peer verification — encrypted but unauthenticated, routed through the existing insecure verifier; elephc does not distinguish self-signed acceptance from a full identity skip). https dispatch priority: cafile → capath → relaxed → peer_name → default trust store - Phase 14 —
vsprintf/vprintf/vfprintf: the array→variadic bridge__rt_vsprintfreads the arguments array (unboxing Mixed-cell slots; reading int/float/bool/string typed slots directly), pushes one 16-byte tagged record per element in reverse order, and tail-calls__rt_sprintf(which formats and pops the records).vprintfwrites the result to stdout;vfprintfwrites it to a stream via__rt_fwrite. All three return PHP-faithful results - Phase 15 — initial
phar://native-PHAR literal read path:fopen("phar://archive.phar/entry", "r")parses the native PHAR manifest at compile time (locating__HALT_COMPILER();, reading the little-endian manifest header and per-entry records, slicing the entry's bytes from the data section) and embeds the uncompressed entry, served through the shared__rt_data_streamhelper — mirroringdata://.stream_get_wrappers()advertisespharhonestly again. Later phases extend this literal path to compressed entries and non-native containers. - Phase 16 —
phar://Milestone-2 (gzip-compressed entries): PHP stores gzip phar entries as raw DEFLATE; the compiler inflates them at compile time viaflate2(pure-Rustminiz_oxidebackend, decompress-only) and embeds the result, so reading a gzip entry throughphar://is transparent - Phase 17 —
phar://Milestone-2b (bzip2-compressed entries): bzip2 phar entries (standardBZhstream) are decompressed at compile time viabzip2-rs(pure-Rust decoder, decompress-only — no system libbz2 or C toolchain, keeping the compiler build portable).phar://now reads uncompressed, gzip, and bzip2 entries - Phase 18 —
crc32()builtin: pure table-free__rt_crc32(reflected polynomial0xEDB88320, init/final XOR0xFFFFFFFF, no system lib), dual-arch, returning the non-negative 32-bit checksum as a 64-bit int. A genuine missing PHP builtin and the prerequisite forphar://writing (PHP verifies per-entry CRC32 on read). Verified against PHP reference vectors - Phase 19 —
stream_socket_enable_cryptoconfirmed real TLS: already a working rustls implementation (elephc_tls_attach_fdover the connected fd, fread/fwrite routed through the session). Added an#[ignore]'d end-to-end test (real HTTPS host, SNI fromssl.peer_name) and corrected the stale docblock. Refinement still open: auto-default SNI to the connection host when no context peer-name is set - Phase 20 (G1a) — userspace-wrapper vtable widened 10→23 reserving the full PHP
StreamWrappersurface (stream_cast/lock/truncate/set_option/metadata/unlink/rename/mkdir/rmdir/dir_*);flock()dispatches tostream_lock(int $operation)(slot 11,__rt_user_wrapper_flock) andftruncate()tostream_truncate(int $new_size)(slot 12,__rt_user_wrapper_ftruncate), threading the operation/size through and returning the wrapper's bool (false when the method is absent); normal fds keep the libc path. ARM64 + x86_64 - Phase 21 (G1b) — userspace-wrapper path-op dispatch:
unlink()(slot 15),rename()(slot 16),mkdir()(slot 17),rmdir()(slot 18) on a registeredscheme://path route to the wrapper's same-named method via the new__rt_user_wrapper_path_op(single-path) /__rt_user_wrapper_rename(two-path) runtime helpers; each builtin gates on__rt_path_is_wrapper(thereadfile()split) and otherwise keeps the libc path. A wrapper missing the method, or a non-wrapper path, returns false / uses the filesystem. ARM64 + x86_64 (both Docker-verified) - Phase 22 (G1c) — userspace-wrapper
stream_metadatadispatch:chmod()on a registeredscheme://path routes to the wrapper'sstream_metadata($path, STREAM_META_ACCESS, $mode)(vtable slot 14) via the shared__rt_user_wrapper_path_ophelper (option/value threaded as the a3/a4 args); a non-wrapper path keeps libc__rt_chmod, a wrapper withoutstream_metadatareturns false. ARM64 + x86_64 (both Docker-verified) - Phase 23 (G1d) — userspace-wrapper
stream_set_optiondispatch:stream_set_blocking()(optionSTREAM_OPTION_BLOCKING) andstream_set_timeout()(optionSTREAM_OPTION_READ_TIMEOUT) on a synthetic wrapper fd route to the wrapper'sstream_set_option($option, $arg1, $arg2)(vtable slot 13) via the new fd-based__rt_user_wrapper_set_optionruntime helper; a normal fd keeps the libcfcntl/setsockoptpath, and a wrapper withoutstream_set_optionreturns false. ARM64 + x86_64 (both Docker-verified) - Phase 24 (G1e) — userspace-wrapper directory iteration:
opendir("scheme://")instantiates the wrapper and callsdir_opendir(vtable slot 19), allocating a handle in the shared_user_wrapper_handlestable and returning the same0x40000000|slotsynthetic fd as a stream handle;readdir/closedir/rewinddirbranch on that fd todir_readdir(20),dir_closedir(21, frees the slot like fclose), anddir_rewinddir(22).__rt_opendirfalls through toglob://+libc when no registered scheme matches. ARM64 + x86_64 (both Docker-verified) - Phase 25 (G1f) — userspace-wrapper
stream_metadataownership dispatch:chown()with an integer uid routes to the wrapper'sstream_metadata($path, STREAM_META_OWNER, $uid)(vtable slot 14) andchgrp()with an integer gid tostream_metadata($path, STREAM_META_GROUP, $gid), via the sharedemit_owner_group_wrapper_dispatchhelper over__rt_user_wrapper_path_op; a non-wrapper path keeps libc__rt_chown, a wrapper withoutstream_metadatareturns false. String owner/group names andtouch()arrays remain libc (deferred, need boxed-Mixed value passing);stream_castdeferred (stream_select cannot select on synthetic fds). ARM64 + x86_64 (both Docker-verified) - Phase 26 (G1g) — userspace-wrapper
stream_metadatavalue as boxedmixed: PHP passesstream_metadata's$valueasmixed, so the dispatch now always boxes the value into an ownedMixedcell (__rt_mixed_from_value), passes the pointer as the method's 4th arg, and releases it with__rt_decref_mixedafter the call (the callee borrows; wrappers declaremixed $value). Completes the metadata surface:chmod()/chown()/chgrp()by integer reuse this;chown()/chgrp()by string name dispatch viaSTREAM_META_OWNER_NAME(2) /STREAM_META_GROUP_NAME(4) through the newemit_owner_group_name_wrapper_dispatch;touch()builds the[mtime, atime]int array (STREAM_META_TOUCH= 1) via the new__rt_touch_meta_arrayruntime helper (resolving "now" timestamps through__rt_time, with a refcount-balanced array→Mixed boxing). Non-wrapper paths keep libc. ARM64 + x86_64 (both Docker-verified) - Phase 27 (Tier 1) —
stream_socket_enable_cryptoSNI auto-default to the connection host:stream_socket_clientnow records each connected fd's transport host (scheme/port stripped,__rt_str_persist'd) in a per-fd_stream_connect_hosttable via the new__rt_stash_connect_hosthelper; whenstream_socket_enable_cryptofinds nossl.peer_namecontext option it defaults the SNI / cert-name to that recorded host (matching PHP) before falling back to"localhost". ARM64 + x86_64 (both Docker-verified) - Phase 28 (Tier 1) — stream-filter
$params(4th arg):stream_filter_append/stream_filter_prependnow accept the optional$paramsargument and thread a bare-integer-literal value into the filter at codegen —zlib.deflatecompression level (-1..9, clamped) andbzip2.compressblockSize (1..9, clamped). A non-constant / array$paramskeeps the default; other filters ignore it. Sharedconst_int_paramextractor instream_filter.rs; the arity/signature widened to 4 args. ARM64 + x86_64 (both Docker-verified) - Phase 29 (Tier 2) — user stream-filter bucket-brigade dispatch works for the PHP-canonical 4-arg
filter($in, $out, &$consumed, $closing): intform (the brigade plumbing was already wired; two pre-existing general Mixed bugs blocked the idiom): (1)__rt_mixed_cast_booltreated aMixed-boxed object (tag 6) as falsy, sowhile ($b = stream_bucket_make_writeable($in))never entered — objects are now truthy like PHP; (2)strtoupper/strtolowerread aMixedoperand via a bareemit_expr(stale string registers → empty result) and now coerce throughemit_string_arg(__rt_mixed_cast_string). Both are broad correctness fixes beyond filters. Tests:test_user_filter_4arg_brigade_transforms_via_while_loop,test_mixed_object_is_truthy. ARM64 + x86_64 (both Docker-verified); macOS full suite 5644/0 - Phase 30 (Tier 2) — stream-context
notificationcallback +STREAM_NOTIFY_*: a literal['notification' => <closure|first-class callable>]entry of thestream_context_create/stream_context_set_params$paramsarray is captured at codegen time into a global slot (retained via the rodata-safeemit_retain_current_descriptor).fopen("http://...")fires it at three milestones through the new__rt_http_fire_notificationruntime shim, which builds the 6-element PHP argument array (int $code, int $severity, ?string $message, int $message_code, int $bytes_transferred, int $bytes_max), boxes it as aMixed(indexed-array)cell, and invokes the callback through its descriptor invoker (the offset-56call_user_func_arrayinvoker contract):STREAM_NOTIFY_CONNECT(2, after each connect — fd restored into the carried register afterward so the request still sends),STREAM_NOTIFY_COMPLETED(8, body length in$bytes_transferred), andSTREAM_NOTIFY_FAILURE(9, severity ERR). Bonus fix:stream_context_createused a fixedscc_store_zerolabel that was defined twice when a program created more than one context — now uniquified, so multiplestream_context_createcalls assemble. v1 limits: literal closure / first-class-callable only (string/array/variable callbacks are not fired, slot cleared); single global slot;http://only;$message/$message_codeare null/0; HTTPS/FTP andPROGRESS/FILE_SIZE_IS/redirect/auth milestones deferred. Tests:test_stream_notification_callback_fires_failure_on_refused_connection,_string_callback_not_fired_in_v1,_cleared_by_later_context,_via_set_params,test_stream_context_create_twice_assembles(CONNECT/COMPLETED validated against a live server during development). ARM64 + x86_64 (both Docker-verified) - Phase 31 (Tier 2) — userspace-wrapper
stream_cast(vtable slot 10) +stream_selectwrapper-awareness: synthetic wrapper fds (0x40000000 | slot) cannot be passed toselect(2), so the new__rt_user_wrapper_stream_cast(fd, cast_as)runtime helper invokes the wrapper object'sstream_cast(int $cast_as)method (slot 10) to resolve a real underlying fd — passthrough for ordinary fds,-1when the handle/method is absent, and it unboxes a boxed-Mixedint/resourcereturn (raw: intreturns pass through).stream_selectnow calls it (withSTREAM_CAST_FOR_SELECT= 3) for every descriptor at both the fd_set build and post-select compaction sites, spilling the caller-saved loop registers around the call (ARM64 grows the select frame to 160 bytes; x86_64 to 160 bytes, relying on callee-saved r12/r13 surviving). A wrapper that exposes a real socket fd viastream_castbecomes select()-able; one withoutstream_castis excluded (matching PHP). The common real-fd path is byte-identical (an extratst/test+ branch, no call). Tests:test_stream_select_wrapper_stream_cast_detects_ready,test_stream_select_wrapper_without_stream_cast_excluded. ARM64 + x86_64 (both Docker-verified) - Phase 32 (Tier 2) — TLS client certificates (mutual TLS) + honest
ciphers/security_level: theelephc-tlscrate gainsclient_cert_config(loads a PEM cert chain + unencrypted private key into a rustlswith_client_auth_certClientConfig) plus theelephc_tls_attach_fd_client_cert/elephc_tls_connect_client_certC entry points (crate unit-tested: config builds from a real self-signed cert+key, and rejects missing/cert-less/keyless inputs).stream_socket_enable_cryptoreadsssl.local_cert+ssl.local_pkfrom the active context (via__rt_get_string_context_option) and, when both are present, dispatches the client-cert attach variant (new_elephc_tls_attach_fd_client_cert_fn/_elephc_tls_connect_client_cert_fnfn-pointer slots, published alongside the others); the enable_crypto spill frame grows 32→64 bytes to hold the cert/key ptr/len pairs, and the x86_64 variant passes the 7th argument (key_len) on the stack. A bad cert/key path fails the config load before any network I/O →false.ssl.passphraseis not honored (rustls reads only unencrypted keys).ssl.ciphers(no rustls equivalent for OpenSSL cipher strings) andssl.security_level(rustls picks TLS 1.2/1.3 automatically) are accepted without error but documented as not honored — the rustls-feasible subset. Tests:elephc-tlscrateclient_cert_config_builds_from_valid_cert_and_key/_rejects_missing_and_certless/connect_client_cert_bad_path_returns_minus_one/attach_client_cert_null_paths_returns_minus_one; codegentest_stream_socket_enable_crypto_client_cert_bad_path_fails. Build the staticlib withcargo build -p elephc-tls. ARM64 + x86_64 (both Docker-verified) - Phase 33 (Tier 2) —
stream_set_chunk_size/stream_set_read_buffer/stream_set_write_buffermade real-ish:stream_set_chunk_size($stream, $size)now tracks a per-fd chunk size in the new_stream_chunk_sizetable (indexed by raw fd up to 256, default 8192) and returns the previous value — PHP's observable save/restore contract — instead of always reporting 8192 (out-of-range / synthetic fds report the default without storing; the size does not yet change read granularity, so only the returned value is observable).stream_set_read_buffer/stream_set_write_bufferkeep returning0("success") — the correct result for elephc's unbuffered (direct-syscall) stream model, where the buffer size has no effect. The chunk-size path uses a uniquifiedscs_have_oldlabel so multiplestream_set_chunk_sizecalls in one program assemble. Tests:test_stream_set_chunk_size_returns_previous,test_stream_set_buffer_stubs. ARM64 + x86_64 (both Docker-verified) - Phase 34 (Tier 3) —
phar://write Milestone 1 now produces a PHP-readable signed archive: the write path (fopen("phar://a.phar/e","w")+fwrite+fclose) already assembled a single uncompressed entry, but signatureless (real PHP rejects unsigned phars whenphar.require_hashis on). The manifest now setsPHAR_HDR_SIGNATURE(0x10000), and__rt_phar_write_finalizecomputes a SHA1 over the whole assembled archive (viaCC_SHA1/SHA1, the raw 20 bytes — not the hex__rt_sha1) and appends theraw-sha1 ++ LE32(0x0002 = Phar::SHA1) ++ "GBMB"trailer before writing to disk (both arches). The type checker declaresrequire_linux_builtin_library("crypto")forphar://write modes so Linux links-lcrypto(crc32 is pure asm and needs no lib; macOS uses CommonCrypto in libSystem). Verified: real PHPnew Phar(...)reads an elephc-written entry back. elephc's own phar reader is compile-time, so a runtime-written archive can't be read back in the same program — the test verifies the on-disk signature bytes directly. Test:test_fopen_phar_write_signs_single_entry. Limits: SHA1 only (no OpenSSL key signing), one stream at a time, uncompressed, buffer-bounded. ARM64 + x86_64 (both Docker-verified) - Phase 35 (Tier 3) —
file_put_contents("phar://archive/entry", $data)completes the phar-write M1 surface: it lowers to the same__rt_phar_write_open→__rt_phar_write_append→__rt_phar_write_finalizeruntime asfopen+fwrite+fclose, producing the identical signed single-entry archive and returning the byte count. The type checker declaresrequire_linux_builtin_library("crypto")forfile_put_contentson aphar://literal (same SHA1 → libcrypto need). Verified real PHP reads the entry back. Test:test_file_put_contents_phar_writes_signed_entry. ARM64 + x86_64 (both Docker-verified) - Phase 36 (Tier 3) — runtime
phar://read (non-literal archive path):fopen("phar://".$path."/entry","r")with a non-literal URL now reads+parses the archive at run time instead of only at compile time. New__rt_phar_read_entryruntime helper reads the whole archive via__rt_file_get_contents, replicates the compile-timeparse_phar_entrymanifest walk in assembly (scan__HALT_COMPILER();, skip the stub tail, read manifest header, walk entries summingcompressedsizes to locate the matched uncompressed entry, with per-load bounds checks), and tail-calls__rt_data_streamto materialize the bytes as a tmpfile-backed readable fd. A new__rt_fopen_maybe_phargate (thefopengeneric non-literal path calls it instead of__rt_fopen) routes aphar://read URL to the reader and everything else straight to__rt_fopen; the literal-URL compile-time fast path (embedded bytes) is unchanged. So a program can read a phar it wrote earlier in the same run, or one whose path is only known at run time. Original Phase-36 scope was one named uncompressed entry; later work extends the runtime reader to gzip/bzip2 entries while multi-entry writes remain deferred. Test:test_fopen_phar_runtime_path_reads_entry(reads the 2nd entry via a runtime path, validating the offset-summing walk). ARM64 + x86_64 (both Docker-verified) - Phase 37 (Tier 3) —
file_get_contents("phar://literal/entry"): a literalphar://URL passed tofile_get_contentsnow decodes the entry at compile time (reusingphar_stream::extract_phar_entry— uncompressed, gzip, or bzip2) and embeds the bytes as the string result, the same compile-time model asfopen("phar://...","r"); a missing archive/entry yields PHPfalse. Pure codegen (no new runtime/assembly): the embedded bytes go throughdata.add_stringand the existingbox_file_get_contents_result(null ptr → false). A non-literalphar://file_get_contentspath is read at run time viafopen+stream_get_contents(the runtime reader from Phase 36). Test:test_file_get_contents_phar_literal_entry. ARM64 + x86_64 (both Docker-verified) - Phase 38 (Tier 3) —
file_get_contents("phar://".$path."/entry")(non-literal runtime read): a non-literalphar://URL passed tofile_get_contentsis now read+parsed at run time, completing the symmetry with the Phase-36 non-literalfopenpath. New__rt_file_get_contents_maybe_phargate (thefile_get_contentsgeneric fall-through path calls it instead of__rt_file_get_contents) checks the"phar://"prefix: a match runs__rt_phar_read_entry→ fd, slurps it with__rt_stream_get_contentsinto an owned string, closes the fd, and returns the string (a phar read error / missing entry → null ptr → boxed PHPfalse); everything else tail-calls__rt_file_get_contentsunchanged. Returning thestream_get_contentscopy (rather than a mid-heap slice of the archive buffer) keeps the boxed string safe to own/decref. So a program canfile_get_contents()a phar it wrote earlier in the same run, or one whose path is only known at run time. Both arches hand-written and verified; the x86 gate caught nothing new (mirrored cleanly). Runtime reader scope is now one named entry in native PHAR format, including uncompressed/gzip/bzip2; tar/zip variants and advanced writes remain deferred. Test:test_file_get_contents_phar_runtime_path(write a phar, read it back through a runtime URL; missing entry → false). ARM64 + x86_64 (both Docker-verified) - Phase 39 (G2) — stream-filter
$paramsarray form: the compression filters now honor PHP's canonical associative-array$params(the 4thstream_filter_append/prependarg), not just the bare-int shorthand that already worked.const_int_param(inbuiltins/io/stream_filter.rs) was generalized to take akey+primaryflag and to read a static int from anArrayLiteralAssocentry, sozlib.deflatereads['level' => N](-1..9) andbzip2.compressreads['blocks' => N](blockSize100k 1..9) and['work' => N](workFactor 0..250, previously hardcoded 0 → now threaded intoBZ2_bzCompressIniton both arches). The value must be a compile-time literal (bare int or literal array with static int entries); a non-constant$paramskeeps the defaults. zlibwindowstays fixed at -15 (required for the raw-deflate round-trip withcompress.zlib://) andmemoryis not exposed — documented. Test:test_stream_filter_params_array_form_round_trips(array form for both filters round-trips through the matching decompressor); the bare-inttest_stream_filter_params_compression_level_round_tripsstill passes. Also fixed the stale Docker images (the cachedbzip2-devapk layer predated the Dockerfile line) by--rebuildof both Linux images. ARM64 + x86_64 (both Docker-verified) - Phase 40 —
stream_socket_enable_crypto($stream, false)real mid-stream TLS teardown: the disable path reloads the fd and calls the sharedfclose::emit_tls_session_teardown(sendsclose_notifyvia_elephc_tls_close_fn, clears_tls_sessions[fd], a no-op when no session is attached), leaving the fd a plain TCP socket, then reportstrue— replacing the v1 stub. Also fixed a latent assembler bug: the enable path's hardcoded__rt_ssec_peer_ok_*labels collided whenstream_socket_enable_cryptowas emitted more than once (e.g. enable then disable); uniquified viactx.next_label. ARM64 + x86_64 (both Docker-verified) - Phase 41 —
stream_get_contents($stream, ?$length, $offset = -1): the optional$length(max bytes) and$offset(seek-before-read) arguments are honored. A finite$lengthroutes through__rt_stream_get_contents_bounded, which loops via__rt_freaduntil$lengthbytes are accumulated, EOF is reached, or an empty read is produced;$offset >= 0seeks first (lseek for a normal fd, the wrapper'sstream_seekfor a synthetic fd) and returns PHPfalseif the seek fails; anull/negative$lengthreads to EOF. The read-all/wrapper-drain path remains factored intoemit_read_all_from_fd, and the builtin is typed/codegenerated asstring|false. ARM64 + x86_64 (both Docker-verified) - Phase 42 —
stream_copy_to_stream($from, $to, ?$length, $offset = -1): the optional$length/$offsetare honored via a single capped, feof-gated__rt_fread/__rt_fwriteloop that works for any real/wrapper fd combination (seeks$fromby$offset >= 0and returns PHPfalseif that seek fails, stops at$lengthbytes copied or source EOF;null/negative$lengthcopies to EOF). The no-extra-args fast path (real fds →__rt_stream_copy_to_stream, wrapper fds → compiled loop) is unchanged, and the builtin is typed/codegenerated asint|false. ARM64 + x86_64 (both Docker-verified) - Phase 43 —
file_get_contents()overhttp:///https:///ftp:///ftps://URLs: a literal URL opens the matching wrapper (the fd-producing core of each wrapper factored into a sharedemit_open_fd), slurps the whole body via the TLS-aware__rt_stream_get_contents, persists it to owned heap with__rt_str_persist, and returns it (falseon a failed open) — the same wrappers asfopen(), sofile_get_contentsnow covers every URL schemefopendoes. Non-literal URL strings now route through__rt_file_get_contents_maybe_url, which recognizes runtimehttp://,https://,ftp://, andftps://before falling back to the phar/filesystem helper. Literalhttps:///ftps://URLs pull in-lelephc_tlsvia the checker; non-literalfile_get_contents()links it conservatively because the runtime scheme is unknown. ARM64 + x86_64 (both Docker-verified) - Phase 44 —
phar://tar/zip container reads: literalfopen()/file_get_contents()PHAR URLs and non-literal runtime PHAR URLs can now read native PHAR, tar-based PHAR, and zip-based PHAR containers. The new pure-Rustelephc-pharbridge is built as a staticlib for runtime reads and as an rlib for compile-time literal extraction, keeping generated assembly target-aware without duplicating archive parsers. Native PHAR gzip/bzip2 entries and ZIP deflate entries decode transparently; ZIP64, encrypted ZIP entries, ZIP data descriptors, and the OOPPhar/PharDataAPI remain deferred. Tests:test_fopen_phar_literal_tar_entry,test_file_get_contents_phar_literal_zip_deflate_entry,test_file_get_contents_phar_runtime_tar_entry,test_fopen_phar_runtime_zip_deflate_entry. ARM64 + x86_64 (both Docker-verified) - Phase 45 — native
phar://write read-modify-write:file_put_contents("phar://archive/entry", ...)andfopen("phar://archive/entry", "w")now publish the newelephc_phar_put_entrybridge, so__rt_phar_write_finalizeinserts or replaces one uncompressed entry in an existing native PHAR instead of regenerating a single-entry archive. The pure-Rust bridge parses existing native manifests, decodes gzip/bzip2 inputs before rewriting, emits uncompressed entries with CRC32 fields, and appends PHP's SHA1 PHAR signature trailer. The old assembly writer remains as a single-entry fallback when no bridge pointer is published. Codegen tests now read both the updated entry and an earlier sibling entry back through runtimephar://URLs. The codegen test runner also rebuilds requested bridge staticlibs when their crate sources are newer than the existing archive, preventing stale-symbol links while developing bridge crates. Tests:writes_and_updates_native_phar_entries,test_file_put_contents_phar_preserves_existing_entries,test_fopen_phar_write_preserves_existing_entries. Follow-up phases cover compressed-entry controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - Phase 46 — runtime-built
phar://writes forfile_put_contents(): non-literal paths now publishelephc_phar_put_urland call__rt_file_put_contents_maybe_phar, which checks the runtime string for thephar://prefix and routes only those writes into the native PHAR bridge. The bridge receives the full URL, splits at the.phar/boundary (falling back to the final slash for non-.phararchive names), and reuses the same read-modify-write manifest/signature path as literal writes. Non-PHAR runtime paths still tail-call__rt_file_put_contentsunchanged. Tests:writes_native_phar_entries_from_url,test_file_put_contents_dynamic_phar_url_preserves_existing_entries. Follow-up phases cover compressed-entry controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - Phase 47 — runtime-built
phar://write streams forfopen(): non-literalfopen($path, $mode)now publishes the PHAR URL writer bridge alongside the dynamic reader bridge.__rt_fopen_maybe_pharstill routesr*modes to__rt_phar_read_entry, butw/a/c/xmodes now tail-call__rt_phar_write_open_url, which persists the full runtime URL with__rt_str_persistsofclose()can finalize throughelephc_phar_put_url. Dynamic stream writes therefore preserve sibling entries in native PHAR archives just like literal streams and dynamicfile_put_contents()writes. Test:test_fopen_dynamic_phar_write_preserves_existing_entries. Follow-up phases cover compressed-entry controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - Phase 48 — tar/zip
phar://writes: theelephc-pharbridge now preserves the archive family for existing native PHAR, tar, and ZIP containers, and missing.tar/.ziparchive paths are created in that family instead of native PHAR. Literal write splitting recognizes.phar/,.tar/, and.zip/boundaries; runtime-built URLs use the same suffix-aware split. ZIP output preserves stored/deflated entries, tar output is POSIX ustar, and native PHAR gzip/bzip2 entries keep their compression when replaced. Tests:writes_tar_entries,writes_zip_entries,writes_preserve_gzip_native_phar_entries,writes_preserve_bzip2_native_phar_entries,test_file_put_contents_phar_tar_archive_runtime_readback,test_file_put_contents_phar_zip_archive_runtime_readback. Follow-up phases cover compression controls and concurrent PHAR write streams; private-key signing remains deferred. ARM64 + x86_64 (both Docker-verified) - Phase 49 — concurrent
phar://write streams: write-modefopen()now publishes bufferedelephc-pharstream entrypoints, so literal and runtime-built PHAR URLs receive real synthetic descriptors in the0x50000000..0x50000020range instead of sharing one global0x50000000stream.fwrite()andfclose()dispatch that whole range, and the bridge owns per-descriptor payload/target state until finalization; the old assembly single-stream writer remains as an unlinked-bridge fallback. Tests:concurrent_phar_write_streams_preserve_distinct_entries,test_fopen_concurrent_phar_write_streams_preserve_entries. Deferred: private-key signing. ARM64 + x86_64 (both Docker-verified) - Phase 50 —
Phar/PharDataOOP baseline: the checker now injects builtinPhar,PharData, andPharFileInfoclasses with PHP-facing format/compression/signature constants, constructors that store the archive path, object-local mixed metadata/string stub state, archive-scanned plus object-local entry iteration state,addFromString(),delete(),compressFiles(),decompressFiles(), path helpers, entrygetContent(), and ArrayAccess methods (offsetGet,offsetSet,offsetExists,offsetUnset) lowered as synthetic PHP bodies over the existingphar://file_get_contents()/file_put_contents()/unlink()runtime paths plus the elephc-phar compression/listing bridge. This gives$phar->addFromString("entry", "data"),$phar->delete("entry"),setMetadata()/getMetadata()/hasMetadata()/delMetadata()for strings, arrays, ints, and null,setStub()/getStub(), native-PHARPhar::GZ/Phar::BZ2/Phar::NONEcompression control, ZIPPhar::GZ/Phar::NONEcompression control,$phar["entry"]->getContent()reads throughPharFileInfo,foreach ($phar as $name => $info)for entries scanned from existing native PHAR/tar/ZIP archives and entries written through that object,isset($phar["entry"]), andunset($phar["entry"])coverage for native PHAR plus tar/ZIP containers without new target-specific assembly. Tests:test_phar_oop_array_access_read_write,test_phar_oop_add_from_string_writes_entries,test_phar_oop_metadata_stub_and_path_helpers,test_phar_oop_iteration_tracks_written_entries,test_phar_oop_iteration_scans_existing_archives,test_phar_oop_array_access_unset_deletes_entry,test_phar_oop_delete_method_removes_entries,test_phar_oop_compress_and_decompress_files. Persisted metadata/stub serialization landed in a later change; tar archive-wide compression controls and private-key signing remain deferred.
The streams/sockets subsystem covers the everyday PHP surface end-to-end (file
I/O, all network transports incl. IPv6+DNS, TLS, the wrapper/filter/context
families, and userspace stream_wrapper_register/stream_filter_register). The
items below are intentionally deferred to a later milestone. They are either
niche, genuinely large (multi-week), or blocked by an upstream-library limit —
none are needed for typical stream usage.
- User stream-filter
$params— the 4thstream_filter_append/prependargument is honored for the built-in compression filters (Phase 39) and is now exposed to userspace filters as$this->paramson classes extending PHP'sphp_user_filterbase class. The optional value is boxed once, passed through__rt_stream_filter_attach_user, seeded beforeonCreate(), and owned by the instantiated filter object. -
phar://compressed runtime reads — gzip/bzip2 entries now decompress through the runtime reader (__rt_phar_read_entry, non-literal paths) as well as the existing compile-time literal fast path. The EIR dynamicfopen()/file_get_contents()paths publish zlib/libbz2 function pointers into runtime slots only when a non-literal path can reach the PHAR reader. -
phar://advanced writes — native PHAR writes now preserve and update multiple uncompressed entries through a read-modify-write bridge, andfile_put_contents()and write-modefopen()support runtime-builtphar://URLs; tar and ZIP containers are writable through the same bridge, multiple write streams can stay open concurrently, andunlink("phar://archive/entry")removes entries while preserving siblings. Native PHAR and ZIP compression-control writes are supported throughPhar::compressFiles()/decompressFiles()forPhar::GZ,Phar::BZ2, andPhar::NONEon native PHAR, and forPhar::GZ/Phar::NONEon ZIP. Out of scope for this closed stream milestone: tar archive-wide compression rewrites and OpenSSL/private-key signing. -
phar://tar/zip variants — native PHAR, tar-based PHAR, and zip-based PHAR containers are readable and writable through literal and runtime PHAR URLs. ZIP entries written with a streaming data descriptor (general-purpose flag bit 3) are read via the authoritative central-directory sizes. ZIP64 archives (over 65535 entries, or sizes/offsets over 4 GiB) are read and written — verified interchangeable with PHP and Python. Traditional-PKWARE (ZipCrypto) encrypted ZIP entries are read and written with a password set via thePhar/PharData::setZipPassword()compiler extension (when set, zip entries — the stub included — are encrypted on write and decrypted on read; the.phar/signature.binentry stays in the clear). ZipCrypto is cryptographically weak and kept only for compatibility with legacy archives. -
Phar/PharDataOOP API — a baseline constructor/constants,addFromString(),delete(), native-PHARcompressFiles()/decompressFiles()(native PHAR plus ZIPPhar::GZ/Phar::NONE), path helpers,PharFileInfogetContent(), archive-scanned and object-local entry iteration, and ArrayAccess read/write/isset surface is implemented (Phase 50).offsetUnset()deletes archive entries through the PHAR-awareunlink()path. -
Pharpersisted global metadata and stub —setMetadata()/getMetadata()/hasMetadata()/delMetadata()andsetStub()/getStub()persist into the archive file and round-trip across fresh objects and processes (and the PHP interpreter) for native PHAR (manifest field + byte-prefix stub), tar (.phar/.metadata.bin+.phar/stub.php), and zip (EOCD comment +.phar/stub.php); reserved.phar/*control entries are hidden from listings. Backed by new publicserialize()/unserialize()builtins (scalar + nested array subset, byte-for-byte PHP-compatible). -
serialize()/unserialize()objects + references — objects serialize asO:<len>:"<Class>":<count>:{...}with PHP's exact public/protected/private key mangling, honour the__serialize/__unserialize/__sleep/__wakeupmagic methods, and emitr:<index>;back-references for repeated objects (PHP's global value counter);unserialize()rebuilds shared objects as one instance (===identity preserved). Byte-for-byte PHP-compatible; 3-target verified. Deferred: cyclic references inside an object's own properties resolve tonullon read, and the deprecatedSerializable(C:) interface is unsupported. -
PharDatawhole-archive (tar) compression —compress(Phar::GZ)/compress(Phar::BZ2)write a sibling.tar.gz/.tar.bz2and return a freshPharData;decompress()writes the plain.tarback. Compressed archives are read transparently (the bridge detects the gzip/bzip2 wrapper) and are interchangeable with the PHP interpreter. Per-entry native/zip compression stays oncompressFiles(). -
Pharsignatures incl. OpenSSL (RSA) —setSignatureAlgorithm()/getSignature()across native PHAR, tar, and zip phars. Hash algorithms (MD5/SHA1/SHA256/SHA512) andsetSignatureAlgorithm(Phar::OPENSSL, $privateKey)(RSA-SHA1, PEM PKCS#1/PKCS#8 key via the pure-Rustrsacrate). Native PHARs use thedigest/sig ++ flag ++ "GBMB"trailer; tar and zip phars use a.phar/signature.bincontrol entry (LE32(flag) ++ LE32(len) ++ signature) appended last, with the signature computed over the data records (tar) or local entries + central directory + comment (zip), matching php-src.getSignature()returns['hash' => <uppercase hex>, 'hash_type' => ...]. The PHP interpreter verifies elephc-written signatures across all three families (OpenSSL against the matching.pubkey). -
PharFileInfopersisted per-file metadata —setMetadata()/getMetadata()/hasMetadata()/delMetadata()on thePharFileInfoobjects returned by ArrayAccess persist per-entry metadata into the archive file and round-trip across fresh objects and the PHP interpreter for native PHAR (per-entry manifest field), tar (.phar/.metadata/<entry>/.metadata.binside entry), and zip (per-entry central-directory file comment). Sameserialize()scalar + array subset as global metadata; verified byte-compatible by reading elephc-written archives back with the PHP interpreter (including nested entry paths). - TLS
ciphers/security_level— accepted without error but not honored: rustls has no OpenSSL-cipher-string equivalent and selects TLS 1.2/1.3 automatically. Honest no-op by design (upstream limitation), not a planned change. Covered by Phase 32 and a focused context-option regression. (ssl.passphraseis likewise unsupported — rustls reads only unencrypted keys.) - Misc lower-level gaps — true non-blocking semantics beyond the
O_NONBLOCKfcntl: nativeread()paths now distinguishEAGAIN/EWOULDBLOCKfrom EOF, sofread()returns an empty result,fgetc()/fgets()returnfalse, andstream_get_line()also avoids settingfeof()on transient non-blocking misses.realpath_cache_get()andrealpath_cache_size()expose elephc's intentionally empty realpath-cache model ([]and0), whilelchown()/lchgrp()route through libclchown(2)without following symlinks and support numeric IDs plus user/group name resolution.
PDO database access, backed by the driver-agnostic crates/elephc-pdo bridge
staticlib (C ABI, no system database dependency): statically-bundled SQLite plus
the pure-Rust postgres and mysql clients. The PDO / PDOStatement /
PDOException classes are implemented as an elephc-PHP prelude that calls the
bridge through extern "elephc_pdo", so the feature compiles through the normal
class/extern/exception pipeline with no bespoke intrinsics or hand-written
assembly. The DSN prefix (sqlite: / pgsql: / mysql:) selects the driver at
open(). The prelude is injected only when a program references PDO, so non-PDO
binaries never link the bridge.
-
crates/elephc-pdobridge staticlib over bundled SQLite (libsqlite3-sys), C-ABI handle tables for connections/statements,-1sentinels, unit-tested in-memory round-trips -
PDO::__construct(sqlite:/sqlite::memory:DSN,PDOExceptionon failure),exec,query,prepare,lastInsertId,beginTransaction/commit/rollBack,errorCode,errorInfo -
PDOStatement::execute(positional?and named:namebinds with int/float/string/null/bool typing),fetch,fetchAll,fetchColumn,rowCount,columnCount -
PDOStatement::bindValue/bindParam(binds the current value) andsetFetchModewith a stored default fetch mode;resetkeeps bindings while a freshexecute($params)rebinds; positional?and named:nameplaceholders may be mixed in one statement - Fetch modes
FETCH_ASSOC,FETCH_NUM,FETCH_BOTH,FETCH_OBJ;PARAM_*/ATTR_ERRMODE/ERRMODE_*constants;ERRMODE_EXCEPTIONdefault -
PDOStatementis Traversable —foreach ($stmt as $key => $row)walks the result set forward in the current fetch mode with sequential integer keys -
PDO::quote()(SQLite single-quote escaping) andFETCH_COLUMNmode (fetch/fetchAll/foreachyield one column as a scalar; column index viasetFetchMode(PDO::FETCH_COLUMN, $col)) -
getAttribute/setAttribute(ATTR_ERRMODE,ATTR_DRIVER_NAME, constructor options array) and configurable error mode —ERRMODE_EXCEPTION(default, throws),ERRMODE_SILENT(exec→false,query/prepare→ falsy),ERRMODE_WARNING(writes toSTDERR, returns the same) - PostgreSQL (
pdo_pgsql) driver — the bridge crate (crates/elephc-pdo) is now driver-agnostic: each connection/statement handle is tagged with its driver and the DSN prefix (sqlite:/pgsql:) selects it atopen(). PostgreSQL uses the pure-Rustpostgresclient (no system libpq), translates?/:nameplaceholders to$1, $2, …, prepares server-side for column metadata and materializes result sets, decodes int/float/bool/text/null plus the rich types as their text form (numericscale-preserving, date/time/timestamp/timestamptz,uuid,json/jsonb— both read and bound), and supportslastInsertId()vialastval()/currval($sequence). The same PDO prelude drives both drivers; PostgreSQL fixtures (which need a live server) are#[ignore]d intests/codegen/pdo_pgsql.rs - MySQL / MariaDB (
pdo_mysql) driver — a third driver behind the same driver-agnostic bridge/prelude, selected by themysql:DSN prefix. Uses the synchronous pure-Rustmysqlclient with flate2's pure-Rust (miniz_oxide) backend, so the staticlib has nolibz/system-client dependency. Rewrites:nameplaceholders to MySQL's positional?, prepares server-side for column metadata and materializes result sets, decodes int/float/bool/text/null plus the rich types as their text form (DECIMALscale-preserving,DATE/DATETIME/TIMESTAMP/TIME), binds values as nativemysql::Value(server coerces text), and supportslastInsertId()viaAUTO_INCREMENT.getAttribute(ATTR_DRIVER_NAME)now reports the real driver (sqlite/pgsql/mysql) via a new bridge entry point. MySQL fixtures (which need a live server) are#[ignore]d intests/codegen/pdo_mysql.rs - PDO maintenance —
__destructreleases bridge handles automatically (PDO::__destructcloses the connection and finalizes its statements,PDOStatement::__destructfinalizes the statement);PDOStatement::rowCount()is snapshotted per statement atexecute()time so a later statement on the same connection cannot change it; prelude injection uses a precise AST walk over class-reference positions instead of aDebug-string scan; thequote()/errorInfo()driver limitations (SQLite-style quoting, native error codes rather than 5-charSQLSTATE) are documented -
FETCH_CLASS/FETCH_INTO, statement-level error-mode propagation, and process-local persistent connections keyed by the fully materialized DSN - Dynamic property assignment so
FETCH_OBJmaterializes a stdClass directly instead of via a JSON round-trip - Binary/BLOB values with embedded NUL bytes (the text bridge path is NUL-terminated)
- Flow-sensitive type-guard narrowing —
if/elseif/elsechains guarded byis_int()/is_float()/is_string()/is_bool()(and theis_integer/is_long/is_doublealiases) or$var instanceof Classnarrow the guarded variable inside the matching branch, with an optional leading!, complement accumulation across the chain andelse, and post-ifnarrowing when every branch diverges (return/throw/exit/die/never-returning calls); union members are filtered to the guarded type andMixedis refined to it, while concrete non-union types are left unchanged (src/types/checker/stmt_check/narrowing.rs, tested intests/codegen/types/narrowing.rs)
- Null-sentinel collision groundwork — one canonical
NULL_SENTINELconstant (src/codegen/sentinels.rs) replacing seven file-local duplicates,i64::MAX - 1disguises, and rawmovz/movkchains; collision repros locked intests/codegen/null_sentinel/and the incompatibility documented indocs/php/types.md - Tagged null representation behind
--null-repr=tagged/ELEPHC_NULL_REPR— inline two-word{payload, tag}TaggedScalarfor null-capable scalars (miss-capable int array reads, emptyarray_pop/array_shift), tag-aware consumers (echo,var_dump,is_null,??,??=,isset,empty,gettype, casts, arithmetic narrowing,===via Mixed boxing), plain-int sentinel checks removed (full 64-bit int range round-trips, including9223372036854775806), local inference and untyped-param widening aligned; covered on all three targets intests/codegen/null_sentinel/tagged.rs - Flip the default null representation to
Tagged(the collision bullet indocs/php/types.mdnow documents only the--null-repr=sentinelopt-out); the{payload, tag}shape is the convergence point for runtime int-overflow→float promotion
Full OOP + procedural date/time surface (DateTime, DateTimeImmutable, DateTimeInterface, DateTimeZone, DateInterval, DatePeriod, and ~45 procedural functions/aliases), plus the complete ext/calendar extension, implemented as synthetic classes/methods over the date()/mktime()/gmmktime() runtime — no new codegen or assembly.
-
DateTime/DateTimeImmutable— construct (string +?DateTimeZone),format,getTimestamp/setTimestamp,getTimezone/setTimezone,getOffset,getMicrosecond/setMicrosecond,setTime,setDate,setISODate,add/sub,modify,diff,createFromFormat,createFromInterface,createFromMutable/createFromImmutable,createFromTimestamp,getLastErrors; per-object timezone honored informat() -
DateTimeInterfaceformat constants (ATOM,COOKIE,ISO8601,ISO8601_EXPANDED, theRFC*family,RSS,W3C) on the interface and both classes -
DateInterval— ISO 8601 duration parsing,format()(%y %m %d %h %i %s %a %R %r %f %F…),createFromDateString -
DatePeriod—Iteratorover a date range, end-date and recurrence-count forms,EXCLUDE_START_DATE/INCLUDE_END_DATE,createFromISO8601String - PHP 8.3 date exception hierarchy —
DateError/DateObjectError/DateRangeError(extendError);DateException/DateInvalidTimeZoneException/DateInvalidOperationException/DateMalformed{String,IntervalString,PeriodString}Exception(extendException) -
date()/gmdate()specifiers includingO P Z T e I c r u v p B X x;createFromFormat()specifiers includingD l S F M z v O P Z T e X xand the! | # ? * +metas with strict trailing-data rejection -
strtotime()returnsint|false(PHP-compatible failure value;-1stays a valid pre-epoch timestamp); broad relative/ISO/keyword/timezone-suffix grammar - Procedural date/time functions —
mktime/gmmktime,checkdate,getdate,localtime,gettimeofday,microtime,hrtime,idate,strftime/gmstrftime,strptime,date_parse/date_parse_from_format,date_default_timezone_get/set, thedate_*OOP aliases, andtimezone_open/timezone_identifiers_list/timezone_name_get/timezone_offset_get/timezone_name_from_abbr/timezone_version_get - Solar functions —
date_sun_info(), and the deprecateddate_sunrise()/date_sunset()(withSUNFUNCS_RET_*), a faithful bit-exact port of PHP's timelib astronomical algorithm -
ext/calendar—gregoriantojd/juliantojd/frenchtojd/jewishtojdand inverses,cal_to_jd/cal_from_jd/cal_days_in_month/cal_info,jddayofweek,jdmonthname,easter_days/easter_date,unixtojd/jdtounix, and theCAL_*constants (Gregorian, Julian, French Republican, and Jewish calendars; bit-exact vs PHP across the full Serial-Day-Number range) - Bundled IANA timezone-database introspection —
DateTimeZone::getLocation()/getTransitions()/listAbbreviations()andtimezone_location_get/timezone_transitions_get/timezone_abbreviations_list. The three tables are baked from PHP's own timelib data (byte-for-byte identical to PHP) into the pure-Rustcrates/elephc-tzbridge staticlib, linked only into programs that use one of these methods (pay-for-use, like the TLS/PDO/crypto bridges); the transitiontimestring is recomputed at runtime by a proleptic-Gregorian formatter exact toPHP_INT_MIN
Introduce a domain-specific intermediate representation (EIR) between the AST-level optimizer and the assembly emitter, then add a real register allocator.
EIR is a custom, PHP-shaped IR — not Cranelift or LLVM. It preserves the
hand-written-and-commented assembly philosophy while removing the
structural ceiling on optimization that the direct AST → ASM emitter
imposed. See docs/internals/the-ir.md.
- EIR design specification (
docs/internals/the-ir.md) — types, instructions, terminators, effects, ownership, textual format -
src/ir/module — types, instructions, builder, validator, printer - AST → EIR lowering pass — every
ExprKind/StmtKindvariant -
--emit-irCLI flag for diagnostics and snapshot testing - EIR → ASM backend producing semantically equivalent output to the legacy backend (no optimizations yet)
- Default backend switch from AST to EIR, with
--ast-backendretained as an explicit fallback - CI default-EIR gate, frozen fallback smoke coverage, and IR-only benchmark job for parity and regression tracking
- Linear-scan register allocator (Poletto-Sarkar) with liveness analysis, live intervals, allocation table, separate int / float pools, and callee-saved preservation across calls
- Register-pressure mitigations: caller-saved reuse for non-call-crossing intervals; better spill heuristic. The linear-scan allocator now classifies each live interval as call-free (never crosses a clobber point — an instruction/terminator whose lowering emits a call or touches a caller-saved register, per the safe-by-default allowlist in
src/ir_passes/clobber.rs) and assigns call-free intervals from caller-saved pools that need no prologue save/restore (x12–x15/d16–d23on aarch64,rsi/rdi/r8/r9/xmm2–xmm7on x86_64), falling back to callee-saved (x21–x28/d8–d14/rbx) for cross-call values. This notably unlocks register allocation for x86_64 floats (no callee-saved XMM) and integers (callee pool is onlyrbx). The spill heuristic is now use-weighted: under pressure the rarely-used, furthest-reaching interval is evicted first, keeping hot values in registers
Expected outcome: EIR is the default and only active implementation backend in v0.24.x. The legacy AST backend is frozen for diagnostics and removal work only, and ≥15% performance improvement on compute benchmarks after Phase 06 by end of v0.24.x.
Build the IR-level passes that the AST optimizer could not reach now that the EIR backend is the user-facing default.
- Deprecation warning on
--ast-backend; from this point the legacy AST backend is frozen as a diagnostic-only fallback, not a feature/parity target - EIR-only backend documentation updates (
the-codegen.md,the-ir.md) with--ast-backenddocumented only as frozen diagnostic fallback - EIR-only backend release notes with
--ast-backenddocumented only as frozen diagnostic fallback (CHANGELOG.mdv0.23.10) - Fixed-point IR pass driver with validation after each pass in test builds —
src/ir_passes/driver.rsruns registeredIrPasstransforms over each function to a fixed point; in debug/test builds it re-validates the function after every pass (panicking and naming the offending pass on malformed IR) and panics on non-convergence within the iteration cap, with both guards compiled out of--release(cap then stops and proceeds). Shared use-rewriting (RAUW) lives insrc/ir_passes/rewrite.rs. - Identity arithmetic folding (
x + 0,x * 1,x ^ x, etc.) —src/ir_passes/identity_arith.rs, the first registered pass. Fold-to-operand neutralizes the op tonopand redirects uses to the surviving operand (x + 0,x * 1,x | 0,x << 0,x & x,x / 1,x * 1.0, …); fold-to-zero rewrites the op in place toconst_i64 0(x ^ x,x - x,x * 0,x & 0,x % 1). PHP-equivalence preserved: integerx / 0/x % 0still trap, and float additive-zero /* 0.0are excluded for signed-zero/NaNsafety. Fold chains within a sweep resolve transitively. - Peephole patterns: redundant load/store, box/unbox cancellation, string-literal concat folding, paired acquire/release cancellation, redundant
Move/Borrowcleanup —src/ir_passes/peephole/, the second registered pass. Box/unbox folds scalarunbox(box(x)) → x;Move/Borrowfold to their operand when ownership/type are unchanged; scalar load/store value-numbering forwardsload-after-storeand drops self-stores on non-aliasedNonHeaplocals; single-useacquire/releasepairs cancel (refcount-neutral);str_concat(const_str, const_str)folds to an internedconst_str(theIrPasstrait gained&mut DataPoolfor literal interning). All rewrites are dominance-safe, validator-clean, and PHP-equivalent; nested/chained cases converge across driver sweeps. - Dead instruction elimination over the IR CFG —
src/ir_passes/dead_inst.rs, the third registered pass. It computes CFG liveness, walks each block backward from terminator uses plus successor live-in sets, and neutralizes unused result-producing instructions whose effect metadata says they are pure. The pass keeps read-only, allocation, throw/fatal/warn, mutation, refcounting, output, and deopt-capable paths intact; same-block dead chains collapse in one sweep and cross-block cascades converge through the fixed-point driver. This absorbs the former v0.23 "Dead code elimination v3" roadmap line at the EIR level. - Dead store elimination over PHP local slots —
src/ir_passes/dead_store.rs, the fourth registered pass. It computes CFG-aware backward slot liveness and neutralizesstore_localinstructions whose stored value is never read on any path before the slot is overwritten or the function exits (including across block boundaries). The pass is restricted to non-refcountedPhpLocalslots that are exclusively accessed through plainload_local/store_local: assignment lowering surrounds refcounted slots with separateacquire/releaseops, so dropping such a store in isolation would unbalance reference counts, while scalar slots carry no ownership ops and their scope-exit cleanup is a no-op, making removal refcount-neutral. It complements the peephole pass's per-block value-equality store forwarding by removing liveness-dead stores of differing values across the CFG; dead pure values feeding a removed store are then cleaned up by dead instruction elimination through the fixed-point driver. - Branch simplification (constant-condition
CondBr, empty-block jump threading, unreachable block removal) —src/ir_passes/branch_simplify.rs, the fifth registered pass. It foldsCondBr/Switchterminators whose selector is a compile-timeconst_bool/const_i64/const_nullinto an unconditionalBr, threads predecessors through empty (parameterless,nop-only) forwarding blocks to the end of theirBrchain, and neutralizes blocks unreachable from the entry. Unreachable blocks are neutralized in place — terminator set toUnreachable, instructions rewritten tonop— rather than physically removed, soblock.id == index, everyValueDef/ValueId/InstIdslot, andtryhandler block-id tokens stay intact with no renumbering; clearing all value uses keeps the validator's dominance check satisfied. Functions containing exception-handling ops are skipped because their handler blocks are reachable through implicit edges absent from the terminator graph. Removing edges only enlarges dominator sets and threaded blocks carry no definitions, so simplification never invalidates a previously valid use; cascades converge through the fixed-point driver. - Per-block constant propagation over EIR value IDs and local slots —
src/ir_passes/const_fold.rs, the third registered pass (after peephole). It folds pure operations whose operands are all compile-time constants into a singleconst_*instruction in place, keeping the same resultValueId(no RAUW): integeriadd/isub/imul, bitwiseand/or/xor, in-range (0..=63) shifts, unaryineg/ibit_not, floatfadd/fsub/fmul/fneg, signedicmp, and theis_null/is_truthypredicates. Because constants in SSA are program-wide (aconst_*value is that constant at every use), a single forward scan over the instruction table discovers constant operands and collapses chains like(2 + 3) * 4in one sweep. Each fold reproduces exactly what the op's lowering computes at runtime (64-bit wrapping integers, in-range shifts only, exact IEEE floats), so the compiled result is unchanged; the trapping integer division/modulo andNaN-sensitive float division are deliberately not folded, matching identity arithmetic. Propagation through local slots is realized by composition: the peephole's scalar load/store value-numbering forwards a constant stored to a local onto its laterload_localuses, and this pass then folds the resulting constant-operand operation — together, under the fixed-point driver, this is per-block constant propagation over EIR value ids and local slots. Constants surfaced by identity arithmetic feed it too ($argc * 0→const_i64 0→ downstream folds). Dead constant producers are then cleaned up by dead instruction elimination and folded branch conditions by branch simplification. - Dominance analysis for cross-block optimization (
src/ir_passes/dominance.rs) — a read-only sidecar analysis (likeliveness/intervals, not a fixed-point driver pass) computing the dominator tree of each function via the Cooper–Harvey–Kennedy iterative algorithm: walk reachable blocks in reverse postorder, recomputing each block's immediate dominator as the intersection of its already-processed predecessors' idoms (a finger-walk over postorder numbers) until a fixed point.compute_dominancereturns aDominanceInfoexposingimmediate_dominator, reflexivedominates/strictly_dominates, dominator-treechildren(top-down traversal),nearest_common_dominator, andis_reachable. Only blocks reachable from the entry participate; unreachable blocks (whichbranch_simplifyneutralizes in place but leaves in the table) are excluded from the tree and answerfalse/None. The internal idom table is self-rooted at the entry so the intersect and dominance walks terminate without special cases. Adds a sharedcfg::predecessorshelper. This is the foundation for the dominance-aware cross-block passes that follow (CSE, loop detection, LICM). - Common subexpression elimination — per-block, then dominance-aware cross-block (absorbs former v0.23 "Constant propagation v4") —
src/ir_passes/cse.rs, the fourth registered transform (after constant folding). It removes a pure computation whose identical predecessor already dominates it, redirecting the redundant result to the earlier value (RAUW) and neutralizing it tonop. Per-block and cross-block redundancy are handled together in one dominator-tree value-numbering traversal: a scoped hash table maps each pure instruction's key(op, result type, immediate, canonicalized operands)to the value that first computed it, and visiting blocks in dominator-tree preorder means the table holds exactly the definitions that dominate the current block (its own earlier instructions plus those of dominating blocks), so any match is a dominating value and the redirect is dominance-safe; entries a block inserts are dropped when its subtree is done. Only pure (Effects::PURE) instructions that have at least one operand and aNonHeap/Persistentresult are eligible — purity makes the value a function of its operands alone and the ownership bound keeps the rewrite refcount-neutral (the same class DCE may drop); SSA operands are equal-by-value, so identical pure ops on identical operands compute identical results. Nullary constant/address materializations (const_*,data_addr) are deliberately left for the backend to rematerialize rather than CSE'd: keeping a single shared constant live across calls would force it into a callee-saved register or spill slot for the whole span with no work removed, so CSE only targets computations. Functions with exception handlers are skipped (a terminator-graph dominator can be bypassed by a throw to a handler reachable only through an implicit edge), reusing the sharedcfg::has_exception_handlersguard and the newdominanceanalysis. The same change makes the validator's dominator computation ignore unreachable predecessors so a loop header whose only other predecessor is an unreachablebreak-skipped update block keeps the entry block in its dominator set. Constant operands are unified by value in the key (two distinctconst_i64 1operands compare equal) so repeated computations over constants collapse too: composed with the peephole's load forwarding,($n + 1) * ($n + 1)loads$nonce and computes$n + 1once, and($a * $b) + ($a * $b)collapses to a singleimul. The nullary constants themselves are still left for the backend to rematerialize (only unified as operands, never CSE'd as instructions). Redundant instructions CSE neutralizes leave dead operands that dead instruction elimination removes, all converging through the fixed-point driver. - Loop detection and natural-loop construction (back edges, headers, preheaders) —
src/ir_passes/loops.rs, a read-only sidecar analysis (likedominance/liveness) building the natural-loop forest on top of the dominator tree.compute_loops(func, &dominance)finds back edges (CFG edgeslatch -> headerwhose target dominates their source), merges back edges sharing a header into oneNaturalLoopwith multiple latches, and constructs each loop body as the header plus every block that can reach a latch without passing through the header (backward walk over reachable predecessors that stops at the header). EachNaturalLoopexposesheader,latches, sortedblocks(binary-searchcontains), nestingparent/depth, and a detectedpreheader;LoopInfoanswersinnermost_loop,loop_depth,is_loop_header, andback_edges. Nesting is by block-set containment (immediate parent = smallest enclosing loop), which is exact for the reducible CFGs the lowerer emits. A preheader is detected as the unique reachable out-of-loop predecessor of the header whose only successor is the header — PHP loops lower to slot-based CFGs (loop variable in a local slot, no block parameters), so the init block before the loop is a natural preheader; when entry is shared or conditional no preheader exists and the optimization that needs one inserts it. This is the foundation for the loop-invariant code motion pass that follows. - Loop-invariant code motion for pure operations —
src/ir_passes/licm.rs, the fifth registered transform (after CSE). It moves a pure computation whose operands do not change across a loop out of the loop body into the loop preheader, so it runs once instead of every iteration. Built on the dominance and loop-forest analyses: for each loop it grows an invariant set to a fixed point — an instruction is invariant when each operand is defined by another hoisted instruction from the same loop or has a definition that dominates the preheader. Only pure (Effects::PURE) instructions with at least one operand and aNonHeap/Persistentresult are hoisted; purity makes the result a function of its operands with no mutable-state read and no fault, so evaluating it once in the preheader unconditionally (even when its original block ran only on some iterations) is safe, and the ownership bound keeps the move refcount-neutral. Nullary constant/address materializations are left for the backend to rematerialize rather than kept live across the loop (same policy as CSE). Loops are processed innermost-first with moves applied immediately, so a value invariant in several nested loops reaches the outermost preheader in one run; relocated instructions' resultValueDefs (block + index) are recomputed once at the end. Loops without a detected preheader, and functions with exception handlers, are skipped. Hoisting reach on current PHP is bounded by slot-based locals (loop variables are reloaded each iteration through impureload_local, so invariant source expressions are not yet pure-operand computations); the firing logic is covered by hand-built EIR unit tests, and e2e fixtures verify behavior is preserved on real for/while/nested loops. - Small-function inliner (size threshold 24 instructions, non-recursive, no try/catch, no generators/fibers) (absorbs former v0.23 "Inline small functions") —
src/ir_passes/inline.rs, a module-level phase run byoptimize_modulebefore the per-function fixed-point loop, so the per-function passes optimize the expanded bodies. It splices an eligible callee's blocks into the caller at the call site: arguments are bound into the remapped parameter slots withstore_local, the caller block jumps into the transplanted entry, each calleereturnbecomes abrto a fresh continuation block that carries the result via a block parameter (replace_all_usesrewrites the call result to it), and the original call is neutralized and parked in an unreachable block so value-def records stay consistent. A callee is eligible only when its body is ≤24 non-nopinstructions, it has a 0-parameter entry block (EIR convention), no exception-handling ops, and it is not a generator/fiber wrapper. Recursion is excluded directly and mutually: a call-graph cycle analysis overmodule.functionsmarks every function that can reach itself and never inlines it, and a per-caller fuel cap backstops termination — so a mutually recursive pair compiles instead of expanding forever. Eligibility is restricted to a provably ownership-safe destructor-free boundary and body (no by-ref/variadic params; every parameter, the return and every local slot is a destructor-free type — scalarsint/float/bool/stringand arrays/unions of destructor-free types). The splice replacesreturnwithbr, bypassing the callee's implicit epilogue cleanup, so correctness is preserved by reproducing that cleanup's per-slot decisions during transplant — parameter slots and directly-returned slots are excluded from host-epilogue cleanup (HiddenTemp), matching the callee (borrowed argument / ownership moved to the caller), while ordinary refcounted internal locals stayPhpLocaland are still freed by the host epilogue. The only residual difference, deferring those internal frees to the host epilogue, is unobservable for destructor-free types (no__destruct, no array identity), so refcount/COW behavior is byte-for-byte preserved. Objects, closures, resources,mixed/iterable, buffers, ref-cell/static/global/capture locals, and by-ref params are excluded because their cleanup timing or aliasing cannot be reproduced by a value-copy splice. Two call-site guards complete correctness: arguments must bind to parameter slots without coercion (matching storage types — spread/named-boxed-mixedand int↔float sites stay ordinary calls), and anystringargument must come from a non-scratch source (const_str/load_local), because the spliced body runs the callee's statement-boundary concat-buffer reset in the host frame and would otherwise free an in-flight scratch string argument. Call-site name resolution (CallData immediates andFunctionVariantCallinclude-variant refs) uses snapshots taken before mutation, so the rewrite loop holds&mut Functionwithout ever aliasing&Module(nounsafe). Behavior is identical with--ir-opton or off; covered by hand-built EIR unit tests (scalar and destructor-free string inlining, FVC sites, multi-block callees, plus negative cases: oversize, direct and mutual recursion, generators,try, by-ref params, object-typed locals/returns) and e2e fixtures (scalar and string inlining proven via--emit-ir, discarded results, mutual-recursion compile-and-run, and string/array helpers — including a copy-on-write array param/return — verified byte-for-byte identical with--ir-opton vs off; the existingruntime_gcsuite passes with inlining on). - Pipeline integration in fixed-point order —
optimize_module(src/ir_passes/driver.rs) now runs the whole EIR pipeline to a module-level fixed point instead of "inline once, then optimize". Each round runs the cross-function small-function inliner and then drives the per-function passes (identity → peephole → const-fold → CSE → LICM → DCE → dead-store → branch-simplify) to their own fixed point on every function-like body; the round repeats while either layer reports a change, capped byMAX_MODULE_ITERATIONS.run_function_passesnow returns whether it modified the function so the module loop can detect convergence. Interleaving lets the two layers feed each other: inlined bodies expose new constants/dead code for the function passes, and the simplified functions expose new (smaller) inline candidates — e.g. a callee that is over the 24-instruction threshold before optimization but collapses below it after constant folding and DCE is inlined in a later round (covered by an e2e fixture: a 48-instructioncalcinlined only after folding shrinks it, verified identical with--ir-opton vs off). The first round reproduces the prior single-pass behavior, so later rounds only optimize further and never change semantics; the combined process converges because inlining is bounded over the acyclic candidate call graph and the function passes only simplify. Covered by a driver unit test for the change-reporting protocol plus the e2e fixed-point fixture, with the full suite (includingruntime_gc) green.
Expected outcome: EIR remains the only active backend implementation target, additional 10–20% performance gain on loop-heavy and call-heavy benchmarks, and cumulative ≥30% improvement vs end-of-v0.23 baseline.
Complete PHP image surface on a pure-Rust bridge (crates/elephc-image,
image/imageproc/ab_glyph/tiny-skia/kamadak-exif). Delivered through a
PHP prelude (src/image_prelude.rs) + extern calls, like PDO/Phar, so binaries
stay standalone (no system GD/libpng/libjpeg/ImageMagick). Imagick/Gmagick/Cairo
are semantic reimplementations of their PHP APIs, not byte-identical to the
original C/C++ libraries; operations with no pure-Rust path are documented,
tested, diagnostic-emitting gaps.
- Foundation:
elephc-imagebridge + handle table, prelude injection/detection,IMAGETYPE_*constants, the always-available core (getimagesize,image_type_to_mime_type,image_type_to_extension), and a GD raster round-trip (imagecreatetruecolor/imagecreate,imagecolorallocate(alpha),imagesetpixel,imagesx/imagesy,imagepngto file,imagedestroy) - GD raster I/O:
imagecreatefrom{png,jpeg,gif,bmp,webp}+imagecreatefromstring, outputimage{png,jpeg,gif,bmp,webp}(file or in-memory/stdout) with a binary blob ABI (staging buffer + encode cell viaptr_write_string/ptr_read_string),imageistruecolor,imageresolution,imagetypes,gd_info. WBMP/XBM/XPM/GD/GD2/AVIF are documented gaps (no pure-Rust path). imagecreatefrom* throwImageExceptionon failure instead of returningfalse(elephc cannot pass/narrow aGdImage|falseresult) - GD color handling:
imagecolorat,imagecolorsforindex,imagecolorexact/closest/closesthwb/resolve(+alpha) reduced to packed true-color values,imagecolordeallocate,imagecolorstotal,imagecolortransparent,imagealphablending(blend vs replace in setpixel),imagesavealpha(alpha flattened on encode unless on),imagelayereffect,imagepalettetotruecolor/imagetruecolortopalette(flag flip, no requantize) - GD drawing & fill:
imageline/imagedashedline,imagerectangle/imagefilledrectangle,imagepolygon/imageopenpolygon/imagefilledpolygon,imageellipse/imagefilledellipse,imagearc/imagefilledarc(pie),imagefill/imagefilltoborder,imagesetthickness(hand-rolled Bresenham/parametric/scanline/flood-fill, alpha-blending-aware). Deferred: brush/style/tile drawing modes, clipping, antialias, built-in-font chars (imagechar handled by the text surface) - GD text & fonts (built-in):
imagestring/imagestringup,imagechar/imagecharup,imagefontwidth/imagefontheight, via the public-domainfont8x8glyph set (uniform 8×8 cell for every font number). Deferred: TTF/FreeType (imagettftext/imagettfbbox/imagefttext/imageftbbox, viaab_glyph) — needs a bundled cross-platform test font;imageloadfont(.gdf) - GD transform/copy/filter:
imagecopy/imagecopymerge/imagecopymergegray/imagecopyresized/imagecopyresampled,imagescale,imagecrop/imagecropauto,imageflip,imagerotate(CCW, enlarged canvas),imageaffine/imageaffinematrixconcat,imagefilter(allIMG_FILTER_*),imageconvolution,imagegammacorrect,imagesetinterpolation/imagegetinterpolation,imageinterlace,imageantialias(no-op). New-image results throwImageExceptionon failure (same union-value limit asimagecreatefrom*). Deferred/gaps:imageaffinematrixget(array|floatparam unrepresentable),imageaffine$clip, antialiased drawing, scatter colors array; deprecatedimage2wbmp/jpeg2wbmp/png2wbmpand Windows-onlyimagegrabscreen/imagegrabwindownot provided - Exif + IPTC:
exif_imagetype,exif_read_data/read_exif_data(flat tag array via the pure-Rustkamadak-exifparser),exif_tagname(TIFF/EXIF/GPS/Interop dictionary),exif_thumbnail(IFD1 JPEG thumbnail with by-refwidth/height/image_type),iptcparse(IIM block →record#datasetarrays) andiptcembed(PhotoshopAPP13insertion, replacing any existing one). Documented simplifications: EXIF values rendered as strings (not typed scalars/arrays); no syntheticFILE/COMPUTED/SectionsFoundsections;exif_tagname/exif_thumbnailreturn""(notfalse) on the not-found path (string|falsecollapse); only JPEG-compressed thumbnails extracted - Imagick (Imagick, ImagickDraw, ImagickPixel, ImagickPixelIterator,
ImagickKernel): wand = a sequence of frames (each a GD image handle), so every
per-image op reuses the GD bridge.
ImagickimplementsIterator+Countableand covers read/readImageBlob/newImage/addImage, write/writeImage/getImageBlob, set/getImageFormat + compression quality, geometry, resize/scale/thumbnail/crop/ rotate/flip/flop, blur/gaussianBlur/sharpen/negate/modulate, compositeImage (OVER/COPY), drawImage, convolveImage (3×3ImagickKernel::fromMatrix), getImagePixelColor, and multi-frame iteration.ImagickDrawbuffers fill/stroke + line/rectangle/circle/ellipse/point/polygon and replays them via the GD rasterizers.ImagickPixelparses CSS name/#hex/rgb()colors;ImagickPixelIteratorwalks rows of pixels. Documented gaps throw the matching*Exception: unsupported composite operators, non-3×3 kernels /fromBuiltIn,distortImage/liquidRescaleImage/fxImage/waveImage/swirlImage, andannotateImage(FreeType text). Two general elephc bugs found here (stringswitchfirst-case-only; int→float parameter coercion) are now fixed in the EIR lowering, not just worked around - Gmagick (Gmagick, GmagickDraw, GmagickPixel): GraphicsMagick-style
API over the same wand bridge and color helpers as Imagick. Gmagick methods are
fluent (
return $this) and cover read/readImageBlob/newImage/addImage, writeImage/getImageBlob, set/getImageFormat + compression quality, geometry, resize/scale/thumbnail/crop/rotate/flip/flop, blur/gaussianBlur/modulate, compositeImage (OVER/COPY), drawImage, multi-frame navigation (getNumberImages/getImageIndex/nextImage/previousImage/hasNext/hasPrevious), and version/package info.GmagickDrawbuffers fill/stroke + line/rectangle/ellipse/ point/polygon;GmagickPixelparses CSS name/#hex/rgb()colors with get/setColorValue + getColorAsString. Documented gaps throw the matching*Exception: unsupported composite operators,swirlImage/charcoalImage/oilPaintImage/embossImage, andannotateImage/GmagickDraw::annotate(FreeType text). A generalreturn $thisuse-after-free + chained owning-receiver leak surfaced here and was fixed in the EIR method-call ownership lowering - Cairo (CairoContext, CairoImageSurface, CairoMatrix, patterns,
gradients): cairo-style 2D vector drawing on the pure-Rust
tiny-skiarasterizer (newcairo.rsbridge module +tiny-skiadep). CairoImageSurface (PNG) is fully supported: paths (moveTo/lineTo/curveTo/arc/arcNegative/rectangle/closePath), fill/stroke/paint with solid colors, linear and radial gradients (CairoLinearGradient/CairoRadialGradient/CairoSolidPattern), the transform stack (save/restore, translate/scale/rotate/transform/setMatrix), line cap/join/ width and fill rule, andCairoMatrixvalue-object transforms. Paths are built in device space (the CTM maps each point as it is added). Documented gaps throwCairoException: PDF/PS/SVG surfaces, FreeType text (showText/textExtents/ CairoToyFontFace/CairoScaledFont), and surface patterns. Geometry crosses the bridge as fixed-point milli-units packed into i64 pairs; colors as packed RGBA8. Surfaced a pre-existing general bug — mixed int/float positional args to an untyped method param misplace the float (free functions and typed params are unaffected); worked around by typing the Cairo numeric method paramsfloat - Cairo procedural API (common subset): the PECL-style free-function
layer (
cairo_image_surface_create/_from_png,cairo_create, thecairo_set_source_*/path/render/transform ops,cairo_pattern_create_*,cairo_matrix_*,cairo_get_current_point) wrapping theCairo*classes.cairo_image_surface_create_from_pngadds a bridge primitive that decodes a PNG via theimagecrate and premultiplies alpha into the tiny-skia pixmap.cairo_get_current_point/cairo_matrix_transform_pointreturn["x"=>…,"y"=>…]assoc arrays and inline the OOP body (the prelude-internal call carries the declaredarraytype, which would force an unsupported AssocArray→Array(Mixed) conversion if it delegated). The obscure PECL tail (font options, scaled fonts, PDF/PS/SVG surface constructors, ~40 rarely-used helpers) is omitted — omitted functions are genuinely undefined (compile-timeUndefined function:), not stubs. Bridge externs renamedcairo_*→elephc_cairo_*to free thecairo_namespace for the procedural PHP layer, andprogram_uses_imagenow detects thecairo_prefix so pure-procedural programs pull in the prelude
Well-bounded PHP-visible array builtins implemented on the EIR backend. All
target-aware (ARM64 + x86_64), with codegen and error tests; the shared __rt_*
runtime helpers are reused and driven through EIR lowering.
-
array_key_first()/array_key_last()(PHP 7.3) — first/last key in insertion order, boxed asMixed,nullfor empty arrays -
array_is_list()(PHP 8.1) — sequential0..n-1key check (indexed arrays are lists by construction; associative arrays walk the insertion-order chain) -
array_replace()/array_replace_recursive()— right-wins key merge over associative arrays (recursive variant merges when both values at a key are associative arrays) -
array_diff_assoc()/array_intersect_assoc()— key + string-cast-value comparison via the unified__rt_assoc_diff_intersecthelper -
array_merge_recursive()— integer-key renumbering, string-key collisions recurse (both arrays) or combine into a list (scalars) -
array_walk_recursive()— invokes the callback on each non-array leaf, recursing through nested indexed/associative arrays -
array_find()/array_any()/array_all()(PHP 8.4) — predicate callbacks; find returns the first match ornull, any/all return booleans -
array_udiff()/array_uintersect()— difference/intersection with a user comparator ($cmp($a, $b) === 0) -
array_multisort()— sort the first indexed array ascending (stable) and reorder a second array in tandem, both by reference (two scalar-element arrays; flags/descending/multi-key are follow-ups) - Scalar indexed-array inputs for the hash-based functions converted to integer-keyed hashes via
__rt_array_to_hash; result key/value widen toMixedfor heterogeneous inputs soforeachdispatches keys correctly. Callback/comparator builtins reuse the EIR descriptor-callback machinery (string, function, and non-capturing closure callbacks). Hash-based functions accept associative arrays and scalar-element indexed arrays; string/heap-element indexed inputs and the callback/sort element-type limits are documented indocs/php/arrays.md
Optimization work should now be driven by benchmarks, generated assembly size, and 0.x validation rather than by speculative pass work.
-
Whole-program declaration reachability — drop unreachable functions, unused classes, and unused methods (including compiler preludes such as PDO) after AST DCE, with conservative keep-all behavior for
eval, dynamic calls,unserialize, and Reflection, and--with-<crate>force-keep for forced prelude groups -
Curated native dependencies v1 —
elephc native add/install/update/remove/list/doctor/prune, exact comment-preserving manifests and deterministic locks, content-addressed target/ABI/toolchain cache, transactional verified source builds, explicit cache cleanup, and read-only compile-time resolution. PCRE2 10.47 links through an opaque Elephc shim with no production system-library fallback; zlib 1.3.2 proves the catalog/recipe path is generic. This remains separate from Composer packages, Rust bridge crates, userexternlinking, and toolchain installation. -
Generators reimplemented on stackful coroutines (issue #329) — a generator body is compiled by the normal EIR backend and runs on its own coroutine stack (reusing the Fiber runtime), replacing the v1 state-machine lowering on the EIR path.
Generator::throw()now raises the exception at the suspendedyield, so atry/catchinside the generator body handles it and resumes instead of always terminating the generator and propagating to the caller; in-generator method calls, arbitrary control flow, andtry/finallyaroundyieldwork like ordinary functions.yield fromover generators delegates through__rt_gen_delegate(forwarding sent values and returning the innergetReturn()) and over arrays desugars into an iterator loop;send()/getReturn()/closure captures preserved; Generator GC frees the coroutine stack and boxed key/value/return cells. -
Closure rebinding —
Closure::bind(),bindTo(), andClosure::call()rebind a closure to a new receiver; a top-level closure that captures$thisnow binds it correctly instead of losing the receiver, and a by-referenceClosure::bindstored in a variable and called later is tracked as a static callable so the call carries the bound cell directly (__rt_closure_bind) rather than going through the generic descriptor invoker -
New magic methods
__callStatic,__isset, and__unset— a static call to an undeclared method dispatches to__callStatic;isset()/empty()on an undeclared property route through__isset(and only read__getwhen__issetis truthy, so an unset virtual property is empty without ever being read); andunset($obj->prop)on a virtual property calls__unset -
Reflection over functions —
ReflectionFunction(name and parameter counts),getParameters(),ReflectionParameter,ReflectionParameter::getType(), andReflectionNamedType; attribute arguments are exposed in reflection metadata, including float, positional-array, named-argument and associative-array values, references to global and class constants, and enum-case references -
References to object properties —
$x = &$obj->propaliases the property with write-through in both directions, and a by-reference function/method return can be captured with$x = &f()(includingstring- andfloat-typed properties); lowered via theLoadPropRefCell/BindRefCellPtrIR ops. Reassigning an array reference to a non-empty literal of a different type boxes the literal's elements to match the property's element type -
Enum case
->nameproperty (issue #330) — every enum case, pure or backed, exposes the read-onlynamestring holding the case identifier (E::A->nameis"A"), matching PHP'sUnitEnum::$name; backed cases keep->value,$this->nameis readable inside enum methods, and access works through direct case access, an aliasing variable,cases(), and string interpolation -
Source maps v2 — richer mappings for functions / expressions / labels and a more stable machine-readable schema for external tooling (
elephc-source-mapversion 2: function ranges with entry symbols, in-function labels, opcode-tagged instruction mappings; schema contract indocs/compiling/source-maps.md) -
Memory-model-aware propagation for heap-backed locals and targeted runtime invalidations beyond
unset($var)and the currently modeled local writes — locals holding all-scalar array literals carry a COW-snapshot fact ($a[<const>]folds,list()unpacks,$b = $acopies), and side-effecting statements now invalidate only the locals they can write: known writes and complexunsettargets stay exact, calls kill their by-ref argument roots (user signatures pre-scanned, builtin signatures from the registry, callback-invoking builtins treated as unknown callees) plus top-level facts only when the callee transitively writesglobalstorage, all backed by a reference-volatility ledger covering&-aliases, by-ref captures/foreach/args,global/staticbindings, and superglobals -
Resource scope-cleanup — auto-free tag-9 resource handles that leave scope without their explicit close (today an unclosed
fopen()leaks its fd and an unfinalizedhash_init()context leaks its heap state until process exit;functions/cleanup.rsskipsResources by design). Prerequisites: a resource-kind subtype in the Mixed cell so the cleanup pass can pick the right destructor (fd →close(), HashContext →elephc_crypto_free, …), and aliasing safety (resources have no refcount;$b = $awould double-free under naive scope-free). Includes wiring the currently-uncalledelephc_crypto_free(_elephc_crypto_free_fnslot + publish entry + a__rt_hash_ctx_freehelper) as the single HashContext destructor and makinghash_finalfinalize a clone (leaving the original owned by its Mixed box) so a finalized context that later leaves scope is freed exactly once — closing the double-final/use-after-free hole documented insrc/codegen_support/runtime/strings/hash_context.rs.popenpipes (kind 3 →__rt_pclose) andopendirstreams (kind 4 →__rt_closedir) are released the same way, and an explicitfclose/pclose/closedirstamps a-1sentinel into the box so a descriptor (whose fd number may be reused) is never closed twice -
Experimental PHP
eval()support — eligible literal fragments are parsed at compile time and lowered to native EIR (including direct or scope-backed caller-local synchronization); dynamic strings and unsupported literal shapes fall back to the optional statically linkedelephc-magicianEvalIR interpreter (auto-linked when required, forced with--with-eval), preserving caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, and ownership/COW semantics within the supported subset -
Output-buffering builtins —
ob_startthroughob_list_handlers(13 builtins) on one shared buffer stack across native and eval'd code, capturing every stdout writer, with user output handlers (closures, first-class callables, function-name strings, eval-registered callables),chunk_sizeauto-flushing, cleanable/flushable/removableflagsgating, and PHP-shapedob_get_status()reporting -
--strict-phpflag — accept only PHP-compatible constructs: extension syntax is rejected with per-violation diagnostics across the main file, includes, and autoloaded files, extension builtins behave exactly as under the PHP interpreter (function_exists()false, undefined-function fatal with a hint naming the disabled extension), and the mode reacheseval()with PHP's execute-time semantics -
declaredirectives (PR #459) —declare(strict_types=1);and the block formdeclare(ticks=1) { ... }parse and are validated syntactically; elephc compiles an always-strict subset, so directives are treated as no-ops -
PHP 8.3 typed class constants — declared types enforced for initializers and inherited overrides (including covariant narrowing) on classes, interfaces, traits, and enums, exposed via
ReflectionClassConstant::hasType()/getType() -
$object::classon object expressions — returns the receiver's concrete runtime class name, evaluates the receiver exactly once, and rejects statically known non-object receivers -
mb_strlen()— nullable optional$encodingargument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, catchableValueErrorfor unknown encodings -
static $x;function-static declarations without an initializer — desugar to= nullin both the native and Magicianeval()parsers, matching PHP -
Purity / may-throw v2 — closed-world instance dispatch now unions concrete override summaries (with exact fixed-construction/final/private targets), named and dynamic property reads distinguish declared untyped slots, typed-slot throws, missing-property warnings, hooks, and
__get, known array offsets separate silent reads from undefined-key warnings, and registry/runtime builtin effects use shared argument-sensitive contracts instead of the previous blanket barrier. A final whole-module fixed point writes these summaries onto refinable EIR call/property instructions before validation, while unresolved/eval/external targets retain conservative defaults -
Guard reasoning v2 for dead-code elimination — integer interval facts from
$x <op> intrelational branches when$xhas a proven integer domain from an exactintparameter, typed local, or literal guard (intersected across nested paths and discharged for transitive relational / strict-int contradictions and impossibleswitchint cases); cross-variable relational / strict-equality atoms with safe complements, full exact coupling after strict substitution, and pure non-throwingwhile/forbody-entry strengthening, still under the path-local ASTGuardStateprotocol with write invalidation, float/string-domain refusal, NaN-safe false-branch policy, and no general CFG join -
Exception-aware DCE v2 — exact thrown-type / handler reachability, nested try rethrow modeling, and less conservative finally-path invalidation
-
Control-flow normalization v2 — broader canonicalization of nested block/control shells before CFG-aware optimization passes
-
Composite conditional include function variants — extend include-graph exclusivity from one direct
if/elseif/elsechain to nested/composed conditional paths where declarations are pairwise exclusive only after combining multiple branch decisions -
Switch-aware conditional include function variants — extend include-graph exclusivity beyond
if/elseif/elsetoswitchcases once fall-through,break, and terminating case bodies are modeled precisely; revisitmatchonly if include-like statement lowering ever appears inside match arms -
Runtime routine dead stripping — include or link only runtime helpers reachable from the generated program instead of carrying the whole target runtime slice
-
Statically-known catchable
Errorconditions (issue #383) — private/protected method access from an inaccessible scope and readonly property writes outside the declaring constructor raise a catchableErrorat runtime instead of being rejected at compile time, matching PHP -
Tail-call optimization — direct tail self- and mutual-recursion lowering on top of EIR (
Brto function entry with parameter rebinding) -
Performance within 2x of C -O0 on compute benchmarks
-
DOOM showcase performance gate after EIR optimizations — build and run a reproducible SDL benchmark for
showcases/doom, track EIR FPS / generated assembly size / runtime helper counts, optionally compare against the last known legacy baseline when available, and require no large real-world regression before release -
Real-world CLI tools compiled as validation
-
Audit remaining references to
--ast-backendand legacy AST emitters so docs, help text, and release notes no longer present a selectable fallback -
Remove the deprecated
--ast-backendCLI flag once diagnostic fallback is no longer needed; report it as unsupported -
Delete frozen legacy AST → ASM emitter modules after shared ABI/runtime dependencies are disentangled
-
Rename
src/codegen_ir/tosrc/codegen/ -
Move historical codegen doc to
docs/internals/legacy-codegen.md(later retired together with the legacy backend); refreshdocs/internals/the-codegen.mdto describe the IR pipeline -
Refresh
docs/internals/the-ir.mdas the canonical, non-preview IR contract for v1.0 -
Apple notarization for direct downloads (codesign + notarytool)
-
Installation / packaging documentation for the supported host platforms — macOS Homebrew, source builds, release artifacts, native toolchain requirements, and managed native dependency prerequisites are covered in
docs/getting-started/installation.md
These are valuable product directions that build on the stabilized 0.x compiler and runtime foundation.
elephc --web app.php compiles a standard PHP file into a standalone prefork
HTTP server binary. The produced binary uses SO_REUSEPORT prefork workers; each
request re-runs the top-level PHP body from a fresh state (globals, function
statics, and static class properties all reset between requests). Run it with
--listen host:port (required) and optionally --workers N (default: CPU count).
- Phase 1 — core serve loop:
elephc --webcompile flag;--listen/--workersruntime args;_elephc_web_handlerentry restructure for per-request invocation;__rt_stdout_writecapture to the response body buffer;__rt_web_resetper-request state reset (function statics, static properties, concat buffer);elephc-webbridge staticlib with prefork/SO_REUSEPORTsupervisor and per-worker hyper HTTP server. Every Phase 1 response is200 OKwith the echoed body. - Phase 2 — request input superglobals:
$_SERVER(method/URI/query +HTTP_*headers +CONTENT_TYPE/CONTENT_LENGTH),$_GET(query string),$_POST(urlencoded body), andphp://input(raw body), built per request by a--webweb prelude and readable inside any function scope (true superglobals via_eir_global_*storage); output-capture completeness so echoed Mixed/array/ resource values reach the response body; superglobals released per request. Not included:$_REQUEST, multipart/form-data. - Phase 3 — response control:
http_response_code()(set/read status, returns previous on set) andheader(), fully PHP-compatible — replace-vs-append,HTTP//Status:status lines,Location:→302, and the third$response_codeargument, all handled in theelephc-webbridge. Web-gated__rt_http_response_code/__rt_headerforward to the bridge under--weband are no-ops otherwise. Not included:Content-Typeis not set automatically (the program controls it). - Phase 4 — hardening:
--max-body-sizerequest body cap (413 on overflow), gracefulSIGINT/SIGTERMshutdown (forward to workers, reap, exit 0), worker respawn on unexpected death, and a 30s header-read timeout bounding slow/idle keep-alive connections. Out of v1 scope: cookies, sessions, TLS, HTTP/2–3, multipart. - Phase 4.1 — compile-time handler isolation: plain
--webpreserves the original in-processworkerpath and performance;--web-isolation=poolselects persistent supervised handler children;requestselects a tracked, disposable child per request. Isolated modes add configurable handler concurrency, body/response deadlines, streaming output, exact dispatch-ID cancellation, PID reaping/replacement, pool-child quotas, and descendant-safe shutdown without adding request-time branching or IPC to the default mode. - Phase 5 — session support:
session_start(),$_SESSIONsuperglobal,session_id(),session_name(),session_status(),session_save_path(),session_write_close()(auto-called at handler end via a finally block),session_regenerate_id(),session_unset(),session_destroy(),session_set_cookie_params()/session_get_cookie_params(), file-based storage (matching PHP'ssession.save_handler = files; an empty configured save path resolves tosys_get_temp_dir()),flock-based concurrency safety,PHP_SESSION_DISABLED/PHP_SESSION_NONE/PHP_SESSION_ACTIVEconstants. - Phase 5.1 — session parity: predefined
PHP_SESSION_*/SIDconstants (no runtimedefine()); custom save handlers via the object formsession_set_save_handler(SessionHandlerInterface)plusSessionHandler,SessionIdInterface,SessionUpdateTimestampHandlerInterface;session.use_strict_mode,serialize_handler(php/php_serialize/php_binary),lazy_write, probabilistic auto-GC (gc_probability/gc_divisor/gc_maxlifetime),sid_length/sid_bits_per_character, completesession_cache_limiter()headers,session_abort()/session_reset()/session_gc()/session_encode()/session_decode()/session_module_name()/session_commit()/session_register_shutdown(). Also supports the legacy 6-callablesession_set_save_handler($open, $close, $read, …)form (deprecated in 8.4) for function-name and closure callables, wired through a general enhancement that letscall_user_func/call_user_func_arraydispatch a boxedMixedcallback by runtime tag; instance array callables[$obj, 'method']in that form now dispatch through the same boxed-Mixedpath (only static['Class', 'method']array callables still need the object form). Fixed a pre-existing CLI SIGSEGV (superglobal type seeding gated on--web) and two EIR ownership bugs onstaticproperty stores: a borrowed object is now acquired (so it dispatches after the borrow is released), and overwriting a Mixed/nullable-object static slot now releases the previous object instead of leaking it on every reassignment. - Phase 5.2 — session runtime configuration:
ini_get()/ini_set()/ini_get_all()scoped to thesession.*directive surface (PHPini_getstring convention — integers as decimals, booleans as'1'/''; unknown directives returnfalse);session.auto_startseeded per worker from theELEPHC_SESSION_AUTO_STARTenvironment variable (auto-starts the session in the request bootstrap,php.ini-PERDIR-style);session.referer_checkenforcement (a cookie-supplied ID whose requestRefererlacks the configured substring is invalidated, starting a fresh session). Adds the ini-settable config surface forsession.use_only_cookies,session.use_trans_sid/trans_sid_*, andsession.upload_progress.*(output URL-rewriting and live upload-progress runtime behavior tracked separately). - Phase 5.3 — php-src lifecycle and storage parity: binary-safe
pointer/length payload ABI; files-handler
[depth;[mode;]]path, nested GC, no-follow/ownership hardening, and complete writes; active callback status, abort/restart/read-and-close/regenerate sequencing; Cookie/GET/POST SID transport without redundant cookies;save_handler,cookie_partitioned,use_cookies, andlazy_writeINI coverage; custom-handler lazy snapshots; andphp_binaryplus non-cookie upload-progress IDs.
-
--emit cdylibflag, export PHP functions as C-callable symbols via#[Export](shipped early; supersedes the planned--libspelling) -
#[Export]attribute for symbol selection (supersedes the planned--exportflag spelling) -
.dylib/.sooutput on all supported targets (macOS aarch64, Linux aarch64, Linux x86_64) -
.astatic library output - Multi-file library compilation
- Symbol visibility control — ELF cdylibs hide every internal global; the dynamic symbol table exposes only
#[Export]trampolines and theelephc_init/elephc_shutdown/elephc_last_error/elephc_freelifecycle entry points - String return values from exported functions (host frees via
elephc_free) - Auto-generated C header file
- Null-terminated string convention for C interop
- Stateful FFI callback trampolines — generate C-ABI-compatible trampoline symbols for descriptor-backed callables passed to extern
callableparameters, retaining descriptor/capture/receiver environments for supported scalar/ptr signatures and documenting constraints for C APIs without userdata/context slots -
pkg-configgeneration - FFI documentation for C, Rust, Python, Go
-
zvalpack/unpack routines (convert elephc values ↔ PHPzvalstructs) - Link against PHP extension
.so/.dylibshared libraries - Bridge for string, int, float, bool, array types
- Proof of concept with one extension (e.g.,
mbstringorcurl) -
--extflag to specify extension libraries at compile time - Documentation: how to bridge a PHP extension
- WASM codegen backend
-
.wat/.wasmemission - WASI support for I/O
- NPM package generation
Features that are feasible but intentionally not on the active 0.x path. They are either product-specific, very high complexity, or better justified by concrete future use cases.
| Feature | Complexity | Notes |
|---|---|---|
| Buffer ergonomics v2 | Medium | Consider dynamic resize/push/pop, foreach, array conversion, and automatic cleanup for buffer<T> while keeping the hot-path POD contract explicit. |
| String-capable FFI callbacks | Medium | Allow C callback signatures that pass or return strings once ownership and temporary C-string lifetimes are modeled safely across callback boundaries. |
| Generator parity v2 | Medium | MVP delivered in v0.21.x for ARM64 and Linux x86_64; yield inside try/catch/finally and exception propagation through Generator::throw landed with the v0.26.x closure work. Remaining parity work: dynamic yield from arrays beyond the compile-time literal form, broader dynamic yield from Iterator targets, and PHP-exact Generator interface inheritance with Iterator. See docs/php/generators.md. |
| Fiber parity v2 | Medium | MVP delivered in v0.20.x for ARM64 and Linux x86_64. Remaining parity work: arithmetic auto-unboxing on mixed payloads received from suspend(), true variadic start(...$args) beyond seven args, dynamic callback targets, by-reference callback start parameters, configurable stack sizing, and PHP-exact FiberError hierarchy. See docs/php/fibers.md. |
| Conditional include class-like variants | High | Keep class/interface/trait/enum duplicate detection strict for now. Supporting branch-selected class-like declarations would require runtime class metadata/layout dispatch, while modern PHP can avoid the ambiguity with namespaces. |
1.0 is not an active planning gate for the current roadmap. Revisit it only after the 0.x compiler/runtime contracts have settled through real-world use.
- Freeze the documented language/runtime contract for the supported target matrix
- Decide which 0.x product tracks belong inside the first stable contract and which remain experimental
- Run a dedicated stabilization pass across compiler, runtime, docs, examples, and packaging
- Ship 1.0 from a proven 0.x baseline