Skip to content

Latest commit

 

History

History
1124 lines (1001 loc) · 163 KB

File metadata and controls

1124 lines (1001 loc) · 163 KB

Roadmap

Direction for upcoming 0.x releases

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.

v0.1.x — Usable CLI compiler (done)

  • 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-level break 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 / $argv superglobals
  • exit($code); / die();
  • Built-in strlen(), intval()
  • Error messages with line/column numbers

v0.2.x — Arrays and null (done)

  • 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 null prints nothing, is_null(), null coercion in operations

v0.3.x — Bool, float, and type system (done)

Proper type system for PHP compatibility.

Bool type

  • true/false as distinct Bool type
  • echo false prints nothing, echo true prints 1 (like PHP)
  • Bool coercion: false0/"" in arithmetic/concat, true1/"1"
  • is_bool(), boolval()
  • === and !== strict comparison (type-aware)

Float type

  • Float literals: 3.14, 1.0e-5, -0.5
  • Division returns float: 10 / 33.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 operations

  • Type casting: (int), (string), (float), (bool), (array)
  • gettype(), settype()
  • empty() — check if variable is empty/falsy
  • unset() — destroy variable

Math functions

  • 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

v0.4.x — Strings (done)

  • 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-Rust crates/elephc-crypto staticlib (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()

v0.5.x — I/O and file system (done)

  • fgets(STDIN) / readline() — read from keyboard
  • STDIN, STDOUT, STDERR constants
  • 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 print output
  • var_dump(), print_r() for debugging

v0.6.x — Associative arrays and switch (done)

  • 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] = 1 after $items = [10, 20]
  • switch / case / default (with fall-through)
  • match expression (PHP 8 style, no fall-through)

v0.7.x — Advanced language features

  • define() / const constants
  • 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) { } with use ($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 above and / xor / or
  • Spaceship operator: <=>
  • call_user_func() (string callbacks)
  • call_user_func_array()
  • function_exists()

v0.8.x — Date/time, JSON, regex

  • 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 and JSON_ERROR_* constants exposed
  • Object encoding via public properties + JsonSerializable dispatch (including nested objects, arrays of objects, and assoc-of-objects)
  • JsonException + RuntimeException class hierarchy (catchable as themselves and any parent class)
  • JSON_PRETTY_PRINT (4-space indent, newlines between elements, single space after :) and JSON_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 as 1.0 instead of collapsing to 1; tail-appends .0 when the formatted slice has no . / e / E marker)
  • JSON_UNESCAPED_UNICODE (multibyte UTF-8 escaped to \uXXXX by 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_str redirects to array_dynamic so the per-element check fires inside indexed string arrays).
  • $depth enforcement: each container encoder (assoc, indexed array, object) increments _json_active_depth at entry, compares with _json_depth_limit, triggers JSON_ERROR_DEPTH (and JsonException under JSON_THROW_ON_ERROR) when the limit is crossed, and decrements on exit so siblings start fresh.
  • JSON_THROW_ON_ERROR for json_encode() and json_decode() errors (raises JsonException with PHP-compatible messages); json_validate() follows PHP's allowed flag set and rejects JSON_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 $depth and routed through __rt_json_throw_error (JSON_ERROR_DEPTH for overflow, JSON_ERROR_SYNTAX for any malformed token).
  • Inf/NaN detection in json_encode() (sets JSON_ERROR_INF_OR_NAN, returns false by default, substitutes 0 with JSON_PARTIAL_OUTPUT_ON_ERROR, and throws with JSON_THROW_ON_ERROR)
  • List-shape detection in associative-array encoder: hashes whose keys form 0..count-1 in insertion order emit JSON arrays ([...]), matching PHP's runtime detection. JSON_FORCE_OBJECT overrides; empty hashes encode as [].
  • Malformed UTF-8 detection in json_encode() (lead-byte validation, continuation-byte validation, bounds-checked truncated sequences) honoring JSON_INVALID_UTF8_IGNORE (silent drop), JSON_INVALID_UTF8_SUBSTITUTE (emit ), and JSON_THROW_ON_ERROR (raises JsonException for JSON_ERROR_UTF8)
  • JSON_PARTIAL_OUTPUT_ON_ERROR semantics: encoder errors return false by 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 structured Mixed value: 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).
  • stdClass builtin class with dynamic property storage. new stdClass() allocates a 16-byte object whose hidden hash backs $obj->name = $val / $obj->name. Property access on stdClass (and on Mixed receivers — the common json_decode($json)->name idiom) routes through __rt_stdclass_get / __rt_stdclass_set (and __rt_mixed_property_get / __rt_mixed_property_set for the unbox+dispatch path). json_decode($json) returns stdClass by default (PHP semantics), json_decode($json, true) returns the assoc array; the _json_decode_assoc runtime flag threads the choice through nested objects. json_encode() of stdClass walks 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 returns Mixed(null) and sets JSON_ERROR_SYNTAX (and raises JsonException under JSON_THROW_ON_ERROR); depth overflow sets JSON_ERROR_DEPTH and raises JsonException likewise. The _json_active_flags/_json_depth_limit/_json_active_depth plumbing is shared with json_validate() and json_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

v0.9.x — Memory management (done)

  • 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_persist skips 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)

v0.10.x — Basic classes (done)

  • Classes with public/private properties and optional defaults
  • Constructor (__construct) with arguments
  • Instance methods with $this access
  • Static methods via ClassName::method()
  • Static properties via ClassName::$prop, self::$prop, parent::$prop, and static::$prop
  • ::class magic constant (Class::class, self::class, parent::class, static::class)
  • new self() / new static() / new parent() factory pattern
  • new keyword for object instantiation
  • -> property access and method calls
  • Nullsafe property access and method calls with ?-> for nullable object receivers
  • readonly properties (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

v0.11.x — Reference-counting garbage collector (done)

  • 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-stats flag: allocations, frees printed to stderr)
  • Strings freed on variable reassignment (value-copied, always owned)

Known limitations

  • Ordinary local/global reassignment now releases previous arrays/objects safely, and indexed array writes / associative-array writes / object property writes / static slots 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

v0.12.x — Math coverage (done)

Trigonometry

  • sin(), cos(), tan()
  • asin(), acos(), atan(), atan2()
  • deg2rad(), rad2deg()
  • sinh(), cosh(), tanh()

Logarithms and exponentials

  • log() — natural logarithm
  • log2(), log10()
  • exp() — e^x

Utility

  • hypot() — sqrt(x² + y²)
  • pi() — alias for M_PI

Constants

  • M_E, M_SQRT2, M_PI_2, M_PI_4, M_LOG2E, M_LOG10E
  • PHP_FLOAT_MIN, PHP_FLOAT_EPSILON

v0.13.x — Pointers

  • Opaque pointer type (ptr) for handles and void*
  • 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 $ptr prints hex address (0x...)
  • Pointer comparison: ===, !== between pointer values

v0.14.x — FFI (Foreign Function Interface)

  • extern function declarations with C type annotations (int, float, string, bool, void, ptr)
  • extern "libname" { } blocks (auto -l linker flag)
  • extern "libname" function name(): type; single-line syntax
  • --link / -l and --link-path / -L CLI flags
  • --framework flag for macOS frameworks
  • Owned null-terminated string ↔ length-prefixed string conversion (__rt_str_to_cstr, __rt_cstr_to_str)
  • extern class for C struct mapping (flat layout, available to ptr_sizeof() and typed pointer field access)
  • extern global for accessing C global variables
  • Callback support: pass elephc functions as C function pointers (callable params)
  • C memory management via extern libc: malloc(), free(), memcpy(), memset()
  • Native interop validation examples: raw FFI memory + SDL2 window/input/framebuffer/audio demos

v0.15.x — Memory model hardening

  • 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-debug with 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

v0.16.x — Language and runtime expansion

  • 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, and self:: / 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-bound static
  • Traits — compile-time method copying / inlining with use, as, insteadof, and trait properties
  • Exceptions (try/catch) — stack unwinding via setjmp/longjmp with runtime frame cleanup and finally support
  • 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
  • protected visibility — third visibility level between public and private
  • Magic methods (__toString, __get, __set) — implicit hooks on property access and string conversion
  • ifdef or similar support

v0.17.x — Language maturity and compiler ergonomics

  • 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
  • match with 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 for int|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-void return-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

v0.18.x — Multi-platform and optimizations

  • Linux x86_64 target
  • Linux ARM64 target
  • Split src/codegen/expr.rs into a slim dispatcher plus smaller focused helpers
  • Split src/codegen/stmt.rs into a slim dispatcher plus smaller focused helpers
  • Target support matrix: macos-aarch64, linux-aarch64, linux-x86_64

v0.19.x — Tooling, compiler throughput, and optimization

  • Runtime object cache — pre-assemble the runtime into ~/.cache/elephc/runtime-<version>-<runtime-hash>.o and 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 + 35 at 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 / finally heuristics — 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 / try shells after pruning
  • Alias-aware constant propagation so local callables and scalar values can stay precise across if / switch / try merges
  • 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
  • print expression form — writes output and returns 1, including statement-form print $x; as ExprStmt(Print(...))
  • Constant propagation v2 — known-subject switch path merges, non-throwing try / unreachable-catch env merges, known match folding, 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 around do...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 / require paths (string literals, concat, magic constants, namespace-aware const / 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 and define() boolean duplicate behavior
  • Static closures: static function() { } and static fn() => ... (no $this capture)
  • 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__0 at lex time instead of silently splitting tokens)
  • Support for never return type
  • iterable pseudo-type runtime parity — foreach over indexed-array, hash-backed, Iterator, and IteratorAggregate iterables; echo, gettype(), var_dump(), ===, scalar casts ((int), (float), (string), (bool)), and the is_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-style 0666 & umask creation mode), chmod(), chown() / chgrp() with numeric IDs or string names, umask() (with the no-arg probe form), ftruncate(), fflush() (implemented as fsync()), fsync(), and fdatasync() (with a Darwin fsync fallback). Runtime paths use libc where practical, with target-aware file-creation handling for touch().
  • Filesystem path manipulation: basename(), dirname() including multi-level parent lookup, pathinfo() (component flags returning strings plus no-flag / PATHINFO_ALL associative-array forms), realpath(), fnmatch() with shell-glob * / ? / [...] / \\ support and zero flags, plus the PATHINFO_DIRNAME / PATHINFO_BASENAME / PATHINFO_EXTENSION / PATHINFO_FILENAME / PATHINFO_ALL constants
  • Filesystem metadata coverage: scalar getters (fileatime(), filectime(), fileperms(), fileowner(), filegroup(), fileinode(), filetype()) with PHP-compatible false on stat failure, permission predicates (is_executable(), is_link(), is_writeable() alias of is_writable()), full PHP-compatible stat() / lstat() / fstat() arrays (numeric 0..=12 + string keys dev/ino/mode/nlink/uid/gid/rdev/size/atime/mtime/ctime/blksize/blocks) with PHP-compatible false on failure, and clearstatcache() as a no-op (elephc has no stat cache)

v0.20.x — PHP filesystem, expression, call, and callable parity closure

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 for FNM_PATHNAME, FNM_PERIOD, FNM_CASEFOLD, and FNM_NOESCAPE
  • Dynamic pathinfo($path, $flag) parity for runtime flags that may evaluate to PATHINFO_ALL; runtime exact PATHINFO_ALL now 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 — return false on open failure while keeping successful handles as resource, and make stream built-ins reject/handle the resource|false path without passing boxed false as a native descriptor
  • Runtime-dynamic include paths — explicitly reject runtime-evaluated include / require path 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 / require targets 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 / require target 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 / else branches 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 instanceof targets — support PHP forms such as $obj instanceof $className with 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 — Fiber and FiberError built-in classes, start() / resume() / suspend() / throw() / getReturn(), state predicates, Fiber::getCurrent(), closure captures, uncaught-exception propagation through the caller, guarded per-fiber mmap stacks, 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::, and parent:: targets
  • Captured closures as callback values — forward hidden use (...) environments through callback-style built-ins such as array_map, array_filter, and call_user_func

JSON parity polish (post v0.8.x base surface)

  • JSON_BIGINT_AS_STRING in json_decode(): integer-grammar tokens (no ., no e/E) whose magnitude exceeds PHP_INT_MAX (9223372036854775807) promote to a Mixed(string) preserving the original digits; in-range integers and float-grammar tokens are unaffected. Length-then-lex compare against the threshold strings 9223372036854775807 (positive) / -9223372036854775808 (negative) detects overflow without invoking __rt_atoi (which silently wraps via imul).
  • Lone UTF-16 surrogate detection in json_decode() / json_validate(): every \uXXXX escape in the high-surrogate range (0xD800..0xDBFF) must be immediately followed by a \uYYYY escape in the low-surrogate range (0xDC00..0xDFFF). Unpaired high surrogates and stand-alone low surrogates set JSON_ERROR_UTF16 (10) and raise JsonException under JSON_THROW_ON_ERROR, with the PHP-faithful message Single 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 $depth argument (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_enter keeps the lenient active <= limit rule (used by json_encode() per PHP), and the decode/validate dispatchers pass $depth - 1 as the runtime limit so both surfaces match PHP exactly without forking the depth helper.
  • Exception::$code and Exception::getCode(): int. The constructor now accepts an optional $code = 0 second argument and stores it as a protected int property. JsonException thrown via JSON_THROW_ON_ERROR carries the originating JSON_ERROR_* code, so catch (JsonException $e) { $e->getCode(); } matches PHP exactly (4 = SYNTAX, 1 = DEPTH, 10 = UTF16, 7 = INF_OR_NAN, etc.). User-code new Exception("msg", $code) also surfaces the value through getCode().
  • is_callable($value): bool builtin. Compile-time decision when the value is a string literal (resolves against the catalog + user functions; case-insensitive for builtins) or a Callable-typed expression (closures, first-class callables). Non-literal strings, [$obj, "method"] arrays, and __invoke objects route to a future runtime helper.
  • Cache _json_active_flags in a callee-saved register (x19 ARM64 / r15 x86_64) inside __rt_json_encode_str: 8 reload sites collapse to a single-instruction tst/test against the cached register, eliminating one address-load + memory dereference per HEX_/UNESCAPED_/UTF-8 dispatch in the per-byte escape loop.

v0.21.x — PHP runtime and language parity

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 adds Token::AttrOpen for #[ ; bare # becomes a PHP-style line comment (no longer ambiguous). Parser invokes parse_attribute_lists at every site PHP allows: top-level statements, class/trait/interface members, enum cases, function/method parameters, closures, arrow functions. Attributes preserved in the AST via attributes: Vec<AttributeGroup> on Stmt, ClassProperty, ClassMethod, EnumCaseDecl, plus per-ClassConst storage on the new ClassConst AST type. Class-like declarations gain constants: 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 in types::checker::schema::validation::extract_deprecation and threads through FunctionSig::deprecation.
  • User-defined #[Attribute] declarations — classes marked #[Attribute] parse without error and accept argument lists at usage sites (#[MyAttribute("test")] class C {}).
  • Generators / yield MVP — Generator built-in class with yield, yield $k => $v, generator functions and captured generator closures, $x = yield resume assignment, boxed Generator::send() payload delivery, Generator::throw(), Generator::getReturn(), terminal return <expr>, state-machine codegen backed by heap-allocated GeneratorFrame objects on ARM64 and Linux x86_64, and yield-context validation that rejects yield outside functions or inside try/catch/finally
  • yield from delegation — forward iteration through compile-time array literals, direct generator calls, and local generator variables, including case-insensitive from parsing and cleanup of owned direct-call delegates after completion
  • Filesystem stream extensions: fgetc() (thin wrapper over fread, length 1, returning false at EOF/read failure), readfile() (open + chunked read+write to stdout + close, returns bytes copied, -1 on read failure, or false on open failure), fpassthru() (same loop on an already-open fd, returning -1 on read failure), flock() (libc flock with PHP→POSIX LOCK_UN translation, preserves the LOCK_NB flag, and supports the optional $would_block output), and tmpfile() (mkstemp("/tmp/elephc-XXXXXX") + immediate unlink so the file auto-deletes on close, returns a PHP resource|false). Also predefines LOCK_SH=1, LOCK_EX=2, LOCK_UN=3, LOCK_NB=4 constants matching PHP's numbering.
  • Filesystem symbolic links: symlink($target, $link), link($target, $link), readlink($path) (returns owned heap string boxed as Mixed for the string|false convention), and linkinfo($path) (returns st_dev or PHP's -1 failure 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(), plus ReflectionAttribute::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 + array across 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 remaining call_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 with a/an <unit> article offsets, add preg_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_shape into the main associative-array encoding walk. __rt_json_encode_assoc now emits a provisional object form, tracks whether keys remain 0..count-1 while 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, and JSON_FORCE_OBJECT disables compaction.
  • JSON decoder optimization — fused the __rt_json_validate pre-pass into __rt_json_decode_mixed for json_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_flags callee-saved-register cache to __rt_json_encode_assoc and __rt_json_encode_array_dynamic (x19 ARM64 / r15 x86_64). The recursive encoder chain now preserves that cache: __rt_json_encode_object no longer clobbers ARM64 x19, and the x86_64 string encoder keeps r15 dedicated 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_apply post-processor. Eliminates the second buffer walk for JSON_PRETTY_PRINT workloads. Multi-day refactor completed with a _json_indent_depth BSS 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") and is_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 static remains 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.

Standard PHP Library (SPL)

PHP-compatible Standard PHP Library coverage, rolled out in phases.

  • Phase 1 — built-in interfaces: Traversable, Iterator (extends Traversable), IteratorAggregate (extends Traversable), OuterIterator, RecursiveIterator, SeekableIterator, Countable, ArrayAccess, SplObserver, SplSubject, Stringable, JsonSerializable
  • Phase 2 — count($obj) redirects to Countable::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-4 driven, includes vendor/<vendor>/<package>/composer.json. Replaces runtime autoload by inlining every reachable PSR-4 class at compile time
  • spl_autoload_* stubs — register/unregister return true, functions returns [], extensions returns ".inc,.php", call and spl_autoload are 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, and if (file_exists(...)) guards. spl_autoload_unregister removes 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() and foreach see one entry per rule
  • Read autoload.classmap, autoload.files, autoload.psr-0, and autoload-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_exists with literal class name and autoload = 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 with if (true), if (false)/else, or chained elseif and the chosen branch is inlined at compile time
  • sprintf, dirname, basename are supported by the symbolic interpreter — common autoloader patterns like require_once sprintf("%s/%s.php", __DIR__, $name) and dirname(__DIR__) . '/lib/' . $name . '.php' fold at compile time
  • get_declared_classes / get_declared_interfaces / get_declared_traits return AOT introspection snapshots of the compiled symbol set
  • class_alias($orig, $alias) synthesises a subclass at compile time so new $alias() and instanceof $alias work as the user expects
  • autoload.exclude-from-classmap skips matching paths during classmap scanning. Supports glob patterns (*, **, ?) plus the trailing-slash directory shorthand
  • PSR-4 empty namespace prefix "" (root namespace) verified working
  • realpath and pathinfo (with PATHINFO_* flags) added to the symbolic interpreter
  • PSR-0 underscore-class convention (Twig_Loader_Filesystemlib/Twig/Loader/Filesystem.php) verified
  • spl_object_id, spl_object_hash, spl_classes runtime helpers; pointer-based identity, stable per process
  • get_class / get_parent_class resolve via the argument's static type at compile time
  • is_a / is_subclass_of with literal class arg fold at compile time, walking parent chain and implemented interfaces
  • Compile-time warning when a spl_autoload_register closure 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.

v0.22.x — Core parity cleanup before EIR

Close the small dispatch, lvalue, and runtime correctness gaps that should not be carried through a backend migration.

  • Throwable-via-interface getMessage() dispatch fix (pre-existing): catching by Throwable and calling getMessage() on the typed binding returns garbage instead of the message string
  • $obj[$k] subscript syntax for ArrayAccess implementers (read, write, isset, unset paths), including Mixed-boxing for offsets and values
  • Raw pointer memory helpers for FFI/socket showcases: ptr_read16(), ptr_write16(), ptr_read_string(), ptr_write_string()
  • IntrinsicCall foundation for runtime-managed SPL/core objects when direct method interception is still the cleanest implementation path

v0.23.x — SPL containers and iterator runway

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(), and iterator_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); functions iterator_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::method strings, plus call_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() plus IteratorAggregate/InternalIterator runtime 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_ptrs table (__rt_call_object_destructor at 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)

Streams and sockets

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_id introspection, STREAM_*/PSFS_*/FILE_*/GLOB_* constants, stream_isatty/stream_is_local/stream_supports_lock/stream_get_transports/stream_get_wrappers/stream_get_filters stubs
  • Phase 2 — stream_context_* resources, php://memory/temp/stdin/stdout/stderr and data:// pseudo-wrappers, stream_get_contents/stream_copy_to_stream
  • Phase 3 — filter chain (stream_filter_append/prepend/remove) with string.toupper/tolower/rot13/zlib.deflate/zlib.inflate built-ins
  • Phase 4a — TCP socket family (stream_socket_client/server/accept) and the http:// wrapper
  • Phase 4b — elephc-tls staticlib bridge (rustls) and the https:// 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/rewinddir with the glob:// 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, fsockopen with by-reference error outputs, stream_select
  • Phase 10 — user-defined wrappers: stream_wrapper_register("scheme", "Class") plus new $variable() parser/runtime so fopen("scheme://...") instantiates Class and dispatches stream_open/stream_read/stream_write/stream_close/stream_eof/stream_seek through the regular method ABI. fopen returns synthetic descriptors in the 0x40000000+ range; fread/fwrite/fclose/feof/fseek detect 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 so stream_get_contents/fpassthru/fgets/stream_copy_to_stream work on userspace-wrapper descriptors without corrupting the caller's resource cell; fgetc/rewind wrapper dispatch; php://filter/[read=|write=]F/resource=… wrapper; fprintf/fscanf; convert.iconv.<from>/<to> charset filter via libc iconv (macOS auto-links -liconv); ssl.cafile custom CA bundle (plus the existing ssl.verify_peer=0) for https://; socket context options including socket.so_broadcast; pfsockopen. Also previously: compress.bzip2:// and ftps:// (RFC 4217) wrappers, tcp_nodelay/so_reuseport/bindto/ipv6_v6only socket 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\|false operands on x86_64
  • Phase 11 follow-up — bzip2.compress / bzip2.decompress stream filters (libbz2, indirect-fn-pointer pattern; interoperable with PHP's bzcompress/bzdecompress)
  • Phase 11 follow-up — write-direction convert.iconv.* filter (STREAM_FILTER_WRITE transcodes each fwrite via libc iconv through the same indirect-fn-pointer write-filter mechanism; id 12)
  • Phase 11 follow-up — socket.backlog context option (__rt_socket_backlog reads ['socket']['backlog'] and feeds the TCP/IPv6/Unix listen() backlog; default 128) and ftp.resume_pos (REST <N> before RETR, 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() via stream_stat; path-based file_exists()/filesize()/is_file() via url_stat(string $path, int $flags) (vtable 8→10, a __rt_path_is_wrapper scheme matcher, and a shared __rt_box_wrapper_stat_result); readfile() on wrapper URLs (fopen + feof-gated drain + close); fgetcsv() and stream_get_line() on wrapper handles (runtime __rt_fgets/__rt_stream_get_line gained a feof-gated __rt_fread loop 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, via elephc_tls_connect_capath), ssl.peer_name (verify the cert / send SNI for a name other than the connection host, via elephc_tls_connect_peer_name), and ssl.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_vsprintf reads 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). vprintf writes the result to stdout; vfprintf writes 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_stream helper — mirroring data://. stream_get_wrappers() advertises phar honestly 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 via flate2 (pure-Rust miniz_oxide backend, decompress-only) and embeds the result, so reading a gzip entry through phar:// is transparent
  • Phase 17 — phar:// Milestone-2b (bzip2-compressed entries): bzip2 phar entries (standard BZh stream) are decompressed at compile time via bzip2-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 polynomial 0xEDB88320, init/final XOR 0xFFFFFFFF, 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 for phar:// writing (PHP verifies per-entry CRC32 on read). Verified against PHP reference vectors
  • Phase 19 — stream_socket_enable_crypto confirmed real TLS: already a working rustls implementation (elephc_tls_attach_fd over the connected fd, fread/fwrite routed through the session). Added an #[ignore]'d end-to-end test (real HTTPS host, SNI from ssl.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 StreamWrapper surface (stream_cast/lock/truncate/set_option/metadata/unlink/rename/mkdir/rmdir/dir_*); flock() dispatches to stream_lock(int $operation) (slot 11, __rt_user_wrapper_flock) and ftruncate() to stream_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 registered scheme:// 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 (the readfile() 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_metadata dispatch: chmod() on a registered scheme:// path routes to the wrapper's stream_metadata($path, STREAM_META_ACCESS, $mode) (vtable slot 14) via the shared __rt_user_wrapper_path_op helper (option/value threaded as the a3/a4 args); a non-wrapper path keeps libc __rt_chmod, a wrapper without stream_metadata returns false. ARM64 + x86_64 (both Docker-verified)
  • Phase 23 (G1d) — userspace-wrapper stream_set_option dispatch: stream_set_blocking() (option STREAM_OPTION_BLOCKING) and stream_set_timeout() (option STREAM_OPTION_READ_TIMEOUT) on a synthetic wrapper fd route to the wrapper's stream_set_option($option, $arg1, $arg2) (vtable slot 13) via the new fd-based __rt_user_wrapper_set_option runtime helper; a normal fd keeps the libc fcntl/setsockopt path, and a wrapper without stream_set_option returns false. ARM64 + x86_64 (both Docker-verified)
  • Phase 24 (G1e) — userspace-wrapper directory iteration: opendir("scheme://") instantiates the wrapper and calls dir_opendir (vtable slot 19), allocating a handle in the shared _user_wrapper_handles table and returning the same 0x40000000|slot synthetic fd as a stream handle; readdir/closedir/rewinddir branch on that fd to dir_readdir (20), dir_closedir (21, frees the slot like fclose), and dir_rewinddir (22). __rt_opendir falls through to glob://+libc when no registered scheme matches. ARM64 + x86_64 (both Docker-verified)
  • Phase 25 (G1f) — userspace-wrapper stream_metadata ownership dispatch: chown() with an integer uid routes to the wrapper's stream_metadata($path, STREAM_META_OWNER, $uid) (vtable slot 14) and chgrp() with an integer gid to stream_metadata($path, STREAM_META_GROUP, $gid), via the shared emit_owner_group_wrapper_dispatch helper over __rt_user_wrapper_path_op; a non-wrapper path keeps libc __rt_chown, a wrapper without stream_metadata returns false. String owner/group names and touch() arrays remain libc (deferred, need boxed-Mixed value passing); stream_cast deferred (stream_select cannot select on synthetic fds). ARM64 + x86_64 (both Docker-verified)
  • Phase 26 (G1g) — userspace-wrapper stream_metadata value as boxed mixed: PHP passes stream_metadata's $value as mixed, so the dispatch now always boxes the value into an owned Mixed cell (__rt_mixed_from_value), passes the pointer as the method's 4th arg, and releases it with __rt_decref_mixed after the call (the callee borrows; wrappers declare mixed $value). Completes the metadata surface: chmod()/chown()/chgrp() by integer reuse this; chown()/chgrp() by string name dispatch via STREAM_META_OWNER_NAME (2) / STREAM_META_GROUP_NAME (4) through the new emit_owner_group_name_wrapper_dispatch; touch() builds the [mtime, atime] int array (STREAM_META_TOUCH = 1) via the new __rt_touch_meta_array runtime 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_crypto SNI auto-default to the connection host: stream_socket_client now records each connected fd's transport host (scheme/port stripped, __rt_str_persist'd) in a per-fd _stream_connect_host table via the new __rt_stash_connect_host helper; when stream_socket_enable_crypto finds no ssl.peer_name context 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_prepend now accept the optional $params argument and thread a bare-integer-literal value into the filter at codegen — zlib.deflate compression level (-1..9, clamped) and bzip2.compress blockSize (1..9, clamped). A non-constant / array $params keeps the default; other filters ignore it. Shared const_int_param extractor in stream_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): int form (the brigade plumbing was already wired; two pre-existing general Mixed bugs blocked the idiom): (1) __rt_mixed_cast_bool treated a Mixed-boxed object (tag 6) as falsy, so while ($b = stream_bucket_make_writeable($in)) never entered — objects are now truthy like PHP; (2) strtoupper/strtolower read a Mixed operand via a bare emit_expr (stale string registers → empty result) and now coerce through emit_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 notification callback + STREAM_NOTIFY_*: a literal ['notification' => <closure|first-class callable>] entry of the stream_context_create / stream_context_set_params $params array is captured at codegen time into a global slot (retained via the rodata-safe emit_retain_current_descriptor). fopen("http://...") fires it at three milestones through the new __rt_http_fire_notification runtime 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 a Mixed(indexed-array) cell, and invokes the callback through its descriptor invoker (the offset-56 call_user_func_array invoker 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), and STREAM_NOTIFY_FAILURE (9, severity ERR). Bonus fix: stream_context_create used a fixed scc_store_zero label that was defined twice when a program created more than one context — now uniquified, so multiple stream_context_create calls 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_code are null/0; HTTPS/FTP and PROGRESS/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_select wrapper-awareness: synthetic wrapper fds (0x40000000 | slot) cannot be passed to select(2), so the new __rt_user_wrapper_stream_cast(fd, cast_as) runtime helper invokes the wrapper object's stream_cast(int $cast_as) method (slot 10) to resolve a real underlying fd — passthrough for ordinary fds, -1 when the handle/method is absent, and it unboxes a boxed-Mixed int/resource return (raw : int returns pass through). stream_select now calls it (with STREAM_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 via stream_cast becomes select()-able; one without stream_cast is excluded (matching PHP). The common real-fd path is byte-identical (an extra tst/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: the elephc-tls crate gains client_cert_config (loads a PEM cert chain + unencrypted private key into a rustls with_client_auth_cert ClientConfig) plus the elephc_tls_attach_fd_client_cert / elephc_tls_connect_client_cert C entry points (crate unit-tested: config builds from a real self-signed cert+key, and rejects missing/cert-less/keyless inputs). stream_socket_enable_crypto reads ssl.local_cert + ssl.local_pk from 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_fn fn-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.passphrase is not honored (rustls reads only unencrypted keys). ssl.ciphers (no rustls equivalent for OpenSSL cipher strings) and ssl.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-tls crate client_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; codegen test_stream_socket_enable_crypto_client_cert_bad_path_fails. Build the staticlib with cargo 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_buffer made real-ish: stream_set_chunk_size($stream, $size) now tracks a per-fd chunk size in the new _stream_chunk_size table (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_buffer keep returning 0 ("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 uniquified scs_have_old label so multiple stream_set_chunk_size calls 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 when phar.require_hash is on). The manifest now sets PHAR_HDR_SIGNATURE (0x10000), and __rt_phar_write_finalize computes a SHA1 over the whole assembled archive (via CC_SHA1/SHA1, the raw 20 bytes — not the hex __rt_sha1) and appends the raw-sha1 ++ LE32(0x0002 = Phar::SHA1) ++ "GBMB" trailer before writing to disk (both arches). The type checker declares require_linux_builtin_library("crypto") for phar:// write modes so Linux links -lcrypto (crc32 is pure asm and needs no lib; macOS uses CommonCrypto in libSystem). Verified: real PHP new 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_finalize runtime as fopen+fwrite+fclose, producing the identical signed single-entry archive and returning the byte count. The type checker declares require_linux_builtin_library("crypto") for file_put_contents on a phar:// 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_entry runtime helper reads the whole archive via __rt_file_get_contents, replicates the compile-time parse_phar_entry manifest walk in assembly (scan __HALT_COMPILER();, skip the stub tail, read manifest header, walk entries summing compressed sizes to locate the matched uncompressed entry, with per-load bounds checks), and tail-calls __rt_data_stream to materialize the bytes as a tmpfile-backed readable fd. A new __rt_fopen_maybe_phar gate (the fopen generic non-literal path calls it instead of __rt_fopen) routes a phar:// 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 literal phar:// URL passed to file_get_contents now decodes the entry at compile time (reusing phar_stream::extract_phar_entry — uncompressed, gzip, or bzip2) and embeds the bytes as the string result, the same compile-time model as fopen("phar://...","r"); a missing archive/entry yields PHP false. Pure codegen (no new runtime/assembly): the embedded bytes go through data.add_string and the existing box_file_get_contents_result (null ptr → false). A non-literal phar:// file_get_contents path is read at run time via fopen + 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-literal phar:// URL passed to file_get_contents is now read+parsed at run time, completing the symmetry with the Phase-36 non-literal fopen path. New __rt_file_get_contents_maybe_phar gate (the file_get_contents generic 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_contents into an owned string, closes the fd, and returns the string (a phar read error / missing entry → null ptr → boxed PHP false); everything else tail-calls __rt_file_get_contents unchanged. Returning the stream_get_contents copy (rather than a mid-heap slice of the archive buffer) keeps the boxed string safe to own/decref. So a program can file_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 $params array form: the compression filters now honor PHP's canonical associative-array $params (the 4th stream_filter_append/prepend arg), not just the bare-int shorthand that already worked. const_int_param (in builtins/io/stream_filter.rs) was generalized to take a key + primary flag and to read a static int from an ArrayLiteralAssoc entry, so zlib.deflate reads ['level' => N] (-1..9) and bzip2.compress reads ['blocks' => N] (blockSize100k 1..9) and ['work' => N] (workFactor 0..250, previously hardcoded 0 → now threaded into BZ2_bzCompressInit on both arches). The value must be a compile-time literal (bare int or literal array with static int entries); a non-constant $params keeps the defaults. zlib window stays fixed at -15 (required for the raw-deflate round-trip with compress.zlib://) and memory is 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-int test_stream_filter_params_compression_level_round_trips still passes. Also fixed the stale Docker images (the cached bzip2-dev apk layer predated the Dockerfile line) by --rebuild of 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 shared fclose::emit_tls_session_teardown (sends close_notify via _elephc_tls_close_fn, clears _tls_sessions[fd], a no-op when no session is attached), leaving the fd a plain TCP socket, then reports true — replacing the v1 stub. Also fixed a latent assembler bug: the enable path's hardcoded __rt_ssec_peer_ok_* labels collided when stream_socket_enable_crypto was emitted more than once (e.g. enable then disable); uniquified via ctx.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 $length routes through __rt_stream_get_contents_bounded, which loops via __rt_fread until $length bytes are accumulated, EOF is reached, or an empty read is produced; $offset >= 0 seeks first (lseek for a normal fd, the wrapper's stream_seek for a synthetic fd) and returns PHP false if the seek fails; a null/negative $length reads to EOF. The read-all/wrapper-drain path remains factored into emit_read_all_from_fd, and the builtin is typed/codegenerated as string|false. ARM64 + x86_64 (both Docker-verified)
  • Phase 42 — stream_copy_to_stream($from, $to, ?$length, $offset = -1): the optional $length/$offset are honored via a single capped, feof-gated __rt_fread/__rt_fwrite loop that works for any real/wrapper fd combination (seeks $from by $offset >= 0 and returns PHP false if that seek fails, stops at $length bytes copied or source EOF; null/negative $length copies 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 as int|false. ARM64 + x86_64 (both Docker-verified)
  • Phase 43 — file_get_contents() over http:///https:///ftp:///ftps:// URLs: a literal URL opens the matching wrapper (the fd-producing core of each wrapper factored into a shared emit_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 (false on a failed open) — the same wrappers as fopen(), so file_get_contents now covers every URL scheme fopen does. Non-literal URL strings now route through __rt_file_get_contents_maybe_url, which recognizes runtime http://, https://, ftp://, and ftps:// before falling back to the phar/filesystem helper. Literal https:///ftps:// URLs pull in -lelephc_tls via the checker; non-literal file_get_contents() links it conservatively because the runtime scheme is unknown. ARM64 + x86_64 (both Docker-verified)
  • Phase 44 — phar:// tar/zip container reads: literal fopen() / 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-Rust elephc-phar bridge 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 OOP Phar/PharData API 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", ...) and fopen("phar://archive/entry", "w") now publish the new elephc_phar_put_entry bridge, so __rt_phar_write_finalize inserts 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 runtime phar:// 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 for file_put_contents(): non-literal paths now publish elephc_phar_put_url and call __rt_file_put_contents_maybe_phar, which checks the runtime string for the phar:// 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-.phar archive names), and reuses the same read-modify-write manifest/signature path as literal writes. Non-PHAR runtime paths still tail-call __rt_file_put_contents unchanged. 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 for fopen(): non-literal fopen($path, $mode) now publishes the PHAR URL writer bridge alongside the dynamic reader bridge. __rt_fopen_maybe_phar still routes r* modes to __rt_phar_read_entry, but w/a/c/x modes now tail-call __rt_phar_write_open_url, which persists the full runtime URL with __rt_str_persist so fclose() can finalize through elephc_phar_put_url. Dynamic stream writes therefore preserve sibling entries in native PHAR archives just like literal streams and dynamic file_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: the elephc-phar bridge now preserves the archive family for existing native PHAR, tar, and ZIP containers, and missing .tar / .zip archive 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-mode fopen() now publishes buffered elephc-phar stream entrypoints, so literal and runtime-built PHAR URLs receive real synthetic descriptors in the 0x50000000..0x50000020 range instead of sharing one global 0x50000000 stream. fwrite() and fclose() 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 / PharData OOP baseline: the checker now injects builtin Phar, PharData, and PharFileInfo classes 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, entry getContent(), and ArrayAccess methods (offsetGet, offsetSet, offsetExists, offsetUnset) lowered as synthetic PHP bodies over the existing phar:// 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-PHAR Phar::GZ / Phar::BZ2 / Phar::NONE compression control, ZIP Phar::GZ / Phar::NONE compression control, $phar["entry"]->getContent() reads through PharFileInfo, foreach ($phar as $name => $info) for entries scanned from existing native PHAR/tar/ZIP archives and entries written through that object, isset($phar["entry"]), and unset($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.

Streams — remaining work (subsystem considered partially complete; merged as-is)

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 4th stream_filter_append/prepend argument is honored for the built-in compression filters (Phase 39) and is now exposed to userspace filters as $this->params on classes extending PHP's php_user_filter base class. The optional value is boxed once, passed through __rt_stream_filter_attach_user, seeded before onCreate(), 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 dynamic fopen() / 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, and file_put_contents() and write-mode fopen() support runtime-built phar:// URLs; tar and ZIP containers are writable through the same bridge, multiple write streams can stay open concurrently, and unlink("phar://archive/entry") removes entries while preserving siblings. Native PHAR and ZIP compression-control writes are supported through Phar::compressFiles() / decompressFiles() for Phar::GZ, Phar::BZ2, and Phar::NONE on native PHAR, and for Phar::GZ / Phar::NONE on 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 the Phar/PharData::setZipPassword() compiler extension (when set, zip entries — the stub included — are encrypted on write and decrypted on read; the .phar/signature.bin entry stays in the clear). ZipCrypto is cryptographically weak and kept only for compatibility with legacy archives.
  • Phar / PharData OOP API — a baseline constructor/constants, addFromString(), delete(), native-PHAR compressFiles() / decompressFiles() (native PHAR plus ZIP Phar::GZ / Phar::NONE), path helpers, PharFileInfo getContent(), archive-scanned and object-local entry iteration, and ArrayAccess read/write/isset surface is implemented (Phase 50). offsetUnset() deletes archive entries through the PHAR-aware unlink() path.
  • Phar persisted global metadata and stubsetMetadata() / getMetadata() / hasMetadata() / delMetadata() and setStub() / 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 public serialize() / unserialize() builtins (scalar + nested array subset, byte-for-byte PHP-compatible).
  • serialize() / unserialize() objects + references — objects serialize as O:<len>:"<Class>":<count>:{...} with PHP's exact public/protected/private key mangling, honour the __serialize/__unserialize/__sleep/__wakeup magic methods, and emit r:<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 to null on read, and the deprecated Serializable (C:) interface is unsupported.
  • PharData whole-archive (tar) compressioncompress(Phar::GZ) / compress(Phar::BZ2) write a sibling .tar.gz / .tar.bz2 and return a fresh PharData; decompress() writes the plain .tar back. 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 on compressFiles().
  • Phar signatures incl. OpenSSL (RSA)setSignatureAlgorithm() / getSignature() across native PHAR, tar, and zip phars. Hash algorithms (MD5/SHA1/SHA256/SHA512) and setSignatureAlgorithm(Phar::OPENSSL, $privateKey) (RSA-SHA1, PEM PKCS#1/PKCS#8 key via the pure-Rust rsa crate). Native PHARs use the digest/sig ++ flag ++ "GBMB" trailer; tar and zip phars use a .phar/signature.bin control 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).
  • PharFileInfo persisted per-file metadatasetMetadata() / getMetadata() / hasMetadata() / delMetadata() on the PharFileInfo objects 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.bin side entry), and zip (per-entry central-directory file comment). Same serialize() 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.passphrase is likewise unsupported — rustls reads only unencrypted keys.)
  • Misc lower-level gaps — true non-blocking semantics beyond the O_NONBLOCK fcntl: native read() paths now distinguish EAGAIN/EWOULDBLOCK from EOF, so fread() returns an empty result, fgetc()/fgets() return false, and stream_get_line() also avoids setting feof() on transient non-blocking misses. realpath_cache_get() and realpath_cache_size() expose elephc's intentionally empty realpath-cache model ([] and 0), while lchown() / lchgrp() route through libc lchown(2) without following symlinks and support numeric IDs plus user/group name resolution.

Database access — PDO (SQLite, PostgreSQL, MySQL/MariaDB)

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-pdo bridge staticlib over bundled SQLite (libsqlite3-sys), C-ABI handle tables for connections/statements, -1 sentinels, unit-tested in-memory round-trips
  • PDO::__construct (sqlite: / sqlite::memory: DSN, PDOException on failure), exec, query, prepare, lastInsertId, beginTransaction / commit / rollBack, errorCode, errorInfo
  • PDOStatement::execute (positional ? and named :name binds with int/float/string/null/bool typing), fetch, fetchAll, fetchColumn, rowCount, columnCount
  • PDOStatement::bindValue / bindParam (binds the current value) and setFetchMode with a stored default fetch mode; reset keeps bindings while a fresh execute($params) rebinds; positional ? and named :name placeholders may be mixed in one statement
  • Fetch modes FETCH_ASSOC, FETCH_NUM, FETCH_BOTH, FETCH_OBJ; PARAM_* / ATTR_ERRMODE / ERRMODE_* constants; ERRMODE_EXCEPTION default
  • PDOStatement is 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) and FETCH_COLUMN mode (fetch / fetchAll / foreach yield one column as a scalar; column index via setFetchMode(PDO::FETCH_COLUMN, $col))
  • getAttribute / setAttribute (ATTR_ERRMODE, ATTR_DRIVER_NAME, constructor options array) and configurable error mode — ERRMODE_EXCEPTION (default, throws), ERRMODE_SILENT (execfalse, query / prepare → falsy), ERRMODE_WARNING (writes to STDERR, 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 at open(). PostgreSQL uses the pure-Rust postgres client (no system libpq), translates ? / :name placeholders 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 (numeric scale-preserving, date/time/timestamp/timestamptz, uuid, json/jsonb — both read and bound), and supports lastInsertId() via lastval() / currval($sequence). The same PDO prelude drives both drivers; PostgreSQL fixtures (which need a live server) are #[ignore]d in tests/codegen/pdo_pgsql.rs
  • MySQL / MariaDB (pdo_mysql) driver — a third driver behind the same driver-agnostic bridge/prelude, selected by the mysql: DSN prefix. Uses the synchronous pure-Rust mysql client with flate2's pure-Rust (miniz_oxide) backend, so the staticlib has no libz/system-client dependency. Rewrites :name placeholders 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 (DECIMAL scale-preserving, DATE/DATETIME/TIMESTAMP/TIME), binds values as native mysql::Value (server coerces text), and supports lastInsertId() via AUTO_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 in tests/codegen/pdo_mysql.rs
  • PDO maintenance — __destruct releases bridge handles automatically (PDO::__destruct closes the connection and finalizes its statements, PDOStatement::__destruct finalizes the statement); PDOStatement::rowCount() is snapshotted per statement at execute() 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 a Debug-string scan; the quote() / errorInfo() driver limitations (SQLite-style quoting, native error codes rather than 5-char SQLSTATE) 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_OBJ materializes a stdClass directly instead of via a JSON round-trip
  • Binary/BLOB values with embedded NUL bytes (the text bridge path is NUL-terminated)

Type checker

  • Flow-sensitive type-guard narrowing — if / elseif / else chains guarded by is_int() / is_float() / is_string() / is_bool() (and the is_integer / is_long / is_double aliases) or $var instanceof Class narrow the guarded variable inside the matching branch, with an optional leading !, complement accumulation across the chain and else, and post-if narrowing when every branch diverges (return / throw / exit / die / never-returning calls); union members are filtered to the guarded type and Mixed is refined to it, while concrete non-union types are left unchanged (src/types/checker/stmt_check/narrowing.rs, tested in tests/codegen/types/narrowing.rs)

Runtime representation

  • Null-sentinel collision groundwork — one canonical NULL_SENTINEL constant (src/codegen/sentinels.rs) replacing seven file-local duplicates, i64::MAX - 1 disguises, and raw movz/movk chains; collision repros locked in tests/codegen/null_sentinel/ and the incompatibility documented in docs/php/types.md
  • Tagged null representation behind --null-repr=tagged / ELEPHC_NULL_REPR — inline two-word {payload, tag} TaggedScalar for null-capable scalars (miss-capable int array reads, empty array_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, including 9223372036854775806), local inference and untyped-param widening aligned; covered on all three targets in tests/codegen/null_sentinel/tagged.rs
  • Flip the default null representation to Tagged (the collision bullet in docs/php/types.md now documents only the --null-repr=sentinel opt-out); the {payload, tag} shape is the convergence point for runtime int-overflow→float promotion

Date/time and calendar parity

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 in format()
  • DateTimeInterface format constants (ATOM, COOKIE, ISO8601, ISO8601_EXPANDED, the RFC* 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
  • DatePeriodIterator over 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 (extend Error); DateException / DateInvalidTimeZoneException / DateInvalidOperationException / DateMalformed{String,IntervalString,PeriodString}Exception (extend Exception)
  • date() / gmdate() specifiers including O P Z T e I c r u v p B X x; createFromFormat() specifiers including D l S F M z v O P Z T e X x and the ! | # ? * + metas with strict trailing-data rejection
  • strtotime() returns int|false (PHP-compatible failure value; -1 stays 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, the date_* OOP aliases, and timezone_open / timezone_identifiers_list / timezone_name_get / timezone_offset_get / timezone_name_from_abbr / timezone_version_get
  • Solar functions — date_sun_info(), and the deprecated date_sunrise() / date_sunset() (with SUNFUNCS_RET_*), a faithful bit-exact port of PHP's timelib astronomical algorithm
  • ext/calendargregoriantojd / juliantojd / frenchtojd / jewishtojd and inverses, cal_to_jd / cal_from_jd / cal_days_in_month / cal_info, jddayofweek, jdmonthname, easter_days / easter_date, unixtojd / jdtounix, and the CAL_* 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() and timezone_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-Rust crates/elephc-tz bridge staticlib, linked only into programs that use one of these methods (pay-for-use, like the TLS/PDO/crypto bridges); the transition time string is recomputed at runtime by a proleptic-Gregorian formatter exact to PHP_INT_MIN

v0.24.x — EIR introduction and register allocation

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/StmtKind variant
  • --emit-ir CLI 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-backend retained 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 (x12x15/d16d23 on aarch64, rsi/rdi/r8/r9/xmm2xmm7 on x86_64), falling back to callee-saved (x21x28/d8d14/rbx) for cross-call values. This notably unlocks register allocation for x86_64 floats (no callee-saved XMM) and integers (callee pool is only rbx). 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.

v0.25.x — EIR optimization passes and Image support

EIR optimization passes

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-backend documented only as frozen diagnostic fallback
  • EIR-only backend release notes with --ast-backend documented only as frozen diagnostic fallback (CHANGELOG.md v0.23.10)
  • Fixed-point IR pass driver with validation after each pass in test builds — src/ir_passes/driver.rs runs registered IrPass transforms 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 in src/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 to nop and 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 to const_i64 0 (x ^ x, x - x, x * 0, x & 0, x % 1). PHP-equivalence preserved: integer x / 0 / x % 0 still trap, and float additive-zero / * 0.0 are excluded for signed-zero/NaN safety. 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 / Borrow cleanup — src/ir_passes/peephole/, the second registered pass. Box/unbox folds scalar unbox(box(x)) → x; Move/Borrow fold to their operand when ownership/type are unchanged; scalar load/store value-numbering forwards load-after-store and drops self-stores on non-aliased NonHeap locals; single-use acquire/release pairs cancel (refcount-neutral); str_concat(const_str, const_str) folds to an interned const_str (the IrPass trait gained &mut DataPool for 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 neutralizes store_local instructions 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-refcounted PhpLocal slots that are exclusively accessed through plain load_local/store_local: assignment lowering surrounds refcounted slots with separate acquire/release ops, 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 folds CondBr/Switch terminators whose selector is a compile-time const_bool/const_i64/const_null into an unconditional Br, threads predecessors through empty (parameterless, nop-only) forwarding blocks to the end of their Br chain, and neutralizes blocks unreachable from the entry. Unreachable blocks are neutralized in place — terminator set to Unreachable, instructions rewritten to nop — rather than physically removed, so block.id == index, every ValueDef/ValueId/InstId slot, and try handler 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 single const_* instruction in place, keeping the same result ValueId (no RAUW): integer iadd/isub/imul, bitwise and/or/xor, in-range (0..=63) shifts, unary ineg/ibit_not, float fadd/fsub/fmul/fneg, signed icmp, and the is_null/is_truthy predicates. Because constants in SSA are program-wide (a const_* value is that constant at every use), a single forward scan over the instruction table discovers constant operands and collapses chains like (2 + 3) * 4 in 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 and NaN-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 later load_local uses, 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 * 0const_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 (like liveness/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_dominance returns a DominanceInfo exposing immediate_dominator, reflexive dominates/strictly_dominates, dominator-tree children (top-down traversal), nearest_common_dominator, and is_reachable. Only blocks reachable from the entry participate; unreachable blocks (which branch_simplify neutralizes in place but leaves in the table) are excluded from the tree and answer false/None. The internal idom table is self-rooted at the entry so the intersect and dominance walks terminate without special cases. Adds a shared cfg::predecessors helper. 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 to nop. 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 a NonHeap/Persistent result 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 shared cfg::has_exception_handlers guard and the new dominance analysis. The same change makes the validator's dominator computation ignore unreachable predecessors so a loop header whose only other predecessor is an unreachable break-skipped update block keeps the entry block in its dominator set. Constant operands are unified by value in the key (two distinct const_i64 1 operands compare equal) so repeated computations over constants collapse too: composed with the peephole's load forwarding, ($n + 1) * ($n + 1) loads $n once and computes $n + 1 once, and ($a * $b) + ($a * $b) collapses to a single imul. 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 (like dominance/liveness) building the natural-loop forest on top of the dominator tree. compute_loops(func, &dominance) finds back edges (CFG edges latch -> header whose target dominates their source), merges back edges sharing a header into one NaturalLoop with 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). Each NaturalLoop exposes header, latches, sorted blocks (binary-search contains), nesting parent/depth, and a detected preheader; LoopInfo answers innermost_loop, loop_depth, is_loop_header, and back_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 a NonHeap/Persistent result 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' result ValueDefs (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 impure load_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 by optimize_module before 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 with store_local, the caller block jumps into the transplanted entry, each callee return becomes a br to a fresh continuation block that carries the result via a block parameter (replace_all_uses rewrites 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-nop instructions, 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 over module.functions marks 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 — scalars int/float/bool/string and arrays/unions of destructor-free types). The splice replaces return with br, 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 stay PhpLocal and 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-mixed and int↔float sites stay ordinary calls), and any string argument 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 (Call Data immediates and FunctionVariantCall include-variant refs) uses snapshots taken before mutation, so the rewrite loop holds &mut Function without ever aliasing &Module (no unsafe). Behavior is identical with --ir-opt on 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-opt on vs off; the existing runtime_gc suite 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 by MAX_MODULE_ITERATIONS. run_function_passes now 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-instruction calc inlined only after folding shrinks it, verified identical with --ir-opt on 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 (including runtime_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.

Image support (GD, Exif/IPTC, Imagick, Gmagick, Cairo)

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-image bridge + 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, imagepng to file, imagedestroy)
  • GD raster I/O: imagecreatefrom{png,jpeg,gif,bmp,webp} + imagecreatefromstring, output image{png,jpeg,gif,bmp,webp} (file or in-memory/stdout) with a binary blob ABI (staging buffer + encode cell via ptr_write_string/ptr_read_string), imageistruecolor, imageresolution, imagetypes, gd_info. WBMP/XBM/XPM/GD/GD2/AVIF are documented gaps (no pure-Rust path). imagecreatefrom* throw ImageException on failure instead of returning false (elephc cannot pass/narrow a GdImage|false result)
  • 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-domain font8x8 glyph set (uniform 8×8 cell for every font number). Deferred: TTF/FreeType (imagettftext/imagettfbbox/imagefttext/ imageftbbox, via ab_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 (all IMG_FILTER_*), imageconvolution, imagegammacorrect, imagesetinterpolation/ imagegetinterpolation, imageinterlace, imageantialias (no-op). New-image results throw ImageException on failure (same union-value limit as imagecreatefrom*). Deferred/gaps: imageaffinematrixget (array|float param unrepresentable), imageaffine $clip, antialiased drawing, scatter colors array; deprecated image2wbmp/jpeg2wbmp/png2wbmp and Windows-only imagegrabscreen/imagegrabwindow not provided
  • Exif + IPTC: exif_imagetype, exif_read_data/read_exif_data (flat tag array via the pure-Rust kamadak-exif parser), exif_tagname (TIFF/EXIF/GPS/Interop dictionary), exif_thumbnail (IFD1 JPEG thumbnail with by-ref width/height/image_type), iptcparse (IIM block → record#dataset arrays) and iptcembed (Photoshop APP13 insertion, replacing any existing one). Documented simplifications: EXIF values rendered as strings (not typed scalars/arrays); no synthetic FILE/COMPUTED/SectionsFound sections; exif_tagname/exif_thumbnail return "" (not false) on the not-found path (string|false collapse); 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. Imagick implements Iterator + Countable and 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×3 ImagickKernel::fromMatrix), getImagePixelColor, and multi-frame iteration. ImagickDraw buffers fill/stroke + line/rectangle/circle/ellipse/point/polygon and replays them via the GD rasterizers. ImagickPixel parses CSS name/#hex/rgb() colors; ImagickPixelIterator walks rows of pixels. Documented gaps throw the matching *Exception: unsupported composite operators, non-3×3 kernels / fromBuiltIn, distortImage/liquidRescaleImage/fxImage/waveImage/swirlImage, and annotateImage (FreeType text). Two general elephc bugs found here (string switch first-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. GmagickDraw buffers fill/stroke + line/rectangle/ellipse/ point/polygon; GmagickPixel parses CSS name/#hex/rgb() colors with get/setColorValue + getColorAsString. Documented gaps throw the matching *Exception: unsupported composite operators, swirlImage/charcoalImage/ oilPaintImage/embossImage, and annotateImage/GmagickDraw::annotate (FreeType text). A general return $this use-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-skia rasterizer (new cairo.rs bridge module + tiny-skia dep). 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, and CairoMatrix value-object transforms. Paths are built in device space (the CTM maps each point as it is added). Documented gaps throw CairoException: 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 params float
  • Cairo procedural API (common subset): the PECL-style free-function layer (cairo_image_surface_create/_from_png, cairo_create, the cairo_set_source_*/path/render/transform ops, cairo_pattern_create_*, cairo_matrix_*, cairo_get_current_point) wrapping the Cairo* classes. cairo_image_surface_create_from_png adds a bridge primitive that decodes a PNG via the image crate and premultiplies alpha into the tiny-skia pixmap. cairo_get_current_point/cairo_matrix_transform_point return ["x"=>…,"y"=>…] assoc arrays and inline the OOP body (the prelude-internal call carries the declared array type, 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-time Undefined function:), not stubs. Bridge externs renamed cairo_*elephc_cairo_* to free the cairo_ namespace for the procedural PHP layer, and program_uses_image now detects the cairo_ prefix so pure-procedural programs pull in the prelude

Array builtin parity (key/list helpers, associative set-ops, recursive merge/walk)

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 as Mixed, null for empty arrays
  • array_is_list() (PHP 8.1) — sequential 0..n-1 key 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_intersect helper
  • 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 or null, 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 to Mixed for heterogeneous inputs so foreach dispatches 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 in docs/php/arrays.md

v0.26.x — Performance closure, legacy cleanup, and 0.x stabilization

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, user extern linking, 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 suspended yield, so a try/catch inside 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, and try/finally around yield work like ordinary functions. yield from over generators delegates through __rt_gen_delegate (forwarding sent values and returning the inner getReturn()) 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(), and Closure::call() rebind a closure to a new receiver; a top-level closure that captures $this now binds it correctly instead of losing the receiver, and a by-reference Closure::bind stored 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 __get when __isset is truthy, so an unset virtual property is empty without ever being read); and unset($obj->prop) on a virtual property calls __unset

  • Reflection over functions — ReflectionFunction (name and parameter counts), getParameters(), ReflectionParameter, ReflectionParameter::getType(), and ReflectionNamedType; 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->prop aliases the property with write-through in both directions, and a by-reference function/method return can be captured with $x = &f() (including string- and float-typed properties); lowered via the LoadPropRefCell / BindRefCellPtr IR 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 ->name property (issue #330) — every enum case, pure or backed, exposes the read-only name string holding the case identifier (E::A->name is "A"), matching PHP's UnitEnum::$name; backed cases keep ->value, $this->name is 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-map version 2: function ranges with entry symbols, in-function labels, opcode-tagged instruction mappings; schema contract in docs/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 = $a copies), and side-effecting statements now invalidate only the locals they can write: known writes and complex unset targets 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 writes global storage, all backed by a reference-volatility ledger covering &-aliases, by-ref captures/foreach/args, global/static bindings, 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 unfinalized hash_init() context leaks its heap state until process exit; functions/cleanup.rs skips Resources 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 = $a would double-free under naive scope-free). Includes wiring the currently-uncalled elephc_crypto_free (_elephc_crypto_free_fn slot + publish entry + a __rt_hash_ctx_free helper) as the single HashContext destructor and making hash_final finalize 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 in src/codegen_support/runtime/strings/hash_context.rs. popen pipes (kind 3 → __rt_pclose) and opendir streams (kind 4 → __rt_closedir) are released the same way, and an explicit fclose/pclose/closedir stamps a -1 sentinel 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 linked elephc-magician EvalIR 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_start through ob_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_size auto-flushing, cleanable/flushable/removable flags gating, and PHP-shaped ob_get_status() reporting

  • --strict-php flag — 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 reaches eval() with PHP's execute-time semantics

  • declare directives (PR #459) — declare(strict_types=1); and the block form declare(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::class on 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 $encoding argument, UTF-8 malformed-sequence handling, byte-count aliases, iconv-backed multibyte encodings, callable dispatch, catchable ValueError for unknown encodings

  • static $x; function-static declarations without an initializer — desugar to = null in both the native and Magician eval() 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> int relational branches when $x has a proven integer domain from an exact int parameter, typed local, or literal guard (intersected across nested paths and discharged for transitive relational / strict-int contradictions and impossible switch int cases); cross-variable relational / strict-equality atoms with safe complements, full exact coupling after strict substitution, and pure non-throwing while / for body-entry strengthening, still under the path-local AST GuardState protocol 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 / else chain to nested/composed conditional paths where declarations are pairwise exclusive only after combining multiple branch decisions

  • Switch-aware conditional include function variants — extend include-graph exclusivity beyond if / elseif / else to switch cases once fall-through, break, and terminating case bodies are modeled precisely; revisit match only if include-like statement lowering ever appears inside match arms

  • 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 Error conditions (issue #383) — private/protected method access from an inaccessible scope and readonly property writes outside the declaring constructor raise a catchable Error at runtime instead of being rejected at compile time, matching PHP

  • Tail-call optimization — direct tail self- and mutual-recursion lowering on top of EIR (Br to function entry with parameter rebinding)

  • Performance within 2x of C -O0 on compute benchmarks

  • 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-backend and legacy AST emitters so docs, help text, and release notes no longer present a selectable fallback

  • Remove the deprecated --ast-backend CLI 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/ to src/codegen/

  • Move historical codegen doc to docs/internals/legacy-codegen.md (later retired together with the legacy backend); refresh docs/internals/the-codegen.md to describe the IR pipeline

  • Refresh docs/internals/the-ir.md as 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

Later 0.x product tracks

These are valuable product directions that build on the stabilized 0.x compiler and runtime foundation.

Web server (--web) — product track

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 --web compile flag; --listen/--workers runtime args; _elephc_web_handler entry restructure for per-request invocation; __rt_stdout_write capture to the response body buffer; __rt_web_reset per-request state reset (function statics, static properties, concat buffer); elephc-web bridge staticlib with prefork/SO_REUSEPORT supervisor and per-worker hyper HTTP server. Every Phase 1 response is 200 OK with the echoed body.
  • Phase 2 — request input superglobals: $_SERVER (method/URI/query + HTTP_* headers + CONTENT_TYPE/CONTENT_LENGTH), $_GET (query string), $_POST (urlencoded body), and php://input (raw body), built per request by a --web web 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) and header(), fully PHP-compatible — replace-vs-append, HTTP//Status: status lines, Location:→302, and the third $response_code argument, all handled in the elephc-web bridge. Web-gated __rt_http_response_code / __rt_header forward to the bridge under --web and are no-ops otherwise. Not included: Content-Type is not set automatically (the program controls it).
  • Phase 4 — hardening: --max-body-size request body cap (413 on overflow), graceful SIGINT/SIGTERM shutdown (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 --web preserves the original in-process worker path and performance; --web-isolation=pool selects persistent supervised handler children; request selects 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(), $_SESSION superglobal, 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's session.save_handler = files; an empty configured save path resolves to sys_get_temp_dir()), flock-based concurrency safety, PHP_SESSION_DISABLED / PHP_SESSION_NONE / PHP_SESSION_ACTIVE constants.
  • Phase 5.1 — session parity: predefined PHP_SESSION_* / SID constants (no runtime define()); custom save handlers via the object form session_set_save_handler(SessionHandlerInterface) plus SessionHandler, 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, complete session_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-callable session_set_save_handler($open, $close, $read, …) form (deprecated in 8.4) for function-name and closure callables, wired through a general enhancement that lets call_user_func/call_user_func_array dispatch a boxed Mixed callback by runtime tag; instance array callables [$obj, 'method'] in that form now dispatch through the same boxed-Mixed path (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 on static property 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 the session.* directive surface (PHP ini_get string convention — integers as decimals, booleans as '1'/''; unknown directives return false); session.auto_start seeded per worker from the ELEPHC_SESSION_AUTO_START environment variable (auto-starts the session in the request bootstrap, php.ini-PERDIR-style); session.referer_check enforcement (a cookie-supplied ID whose request Referer lacks the configured substring is invalidated, starting a fresh session). Adds the ini-settable config surface for session.use_only_cookies, session.use_trans_sid / trans_sid_*, and session.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, and lazy_write INI coverage; custom-handler lazy snapshots; and php_binary plus non-cookie upload-progress IDs.

v0.27.x — Shared and static libraries (C ABI)

  • --emit cdylib flag, export PHP functions as C-callable symbols via #[Export] (shipped early; supersedes the planned --lib spelling)
  • #[Export] attribute for symbol selection (supersedes the planned --export flag spelling)
  • .dylib / .so output on all supported targets (macOS aarch64, Linux aarch64, Linux x86_64)
  • .a static library output
  • Multi-file library compilation
  • Symbol visibility control — ELF cdylibs hide every internal global; the dynamic symbol table exposes only #[Export] trampolines and the elephc_init/elephc_shutdown/elephc_last_error/elephc_free lifecycle 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 callable parameters, retaining descriptor/capture/receiver environments for supported scalar/ptr signatures and documenting constraints for C APIs without userdata/context slots
  • pkg-config generation
  • FFI documentation for C, Rust, Python, Go

v0.28.x — PHP extension bridge (experimental)

  • zval pack/unpack routines (convert elephc values ↔ PHP zval structs)
  • Link against PHP extension .so / .dylib shared libraries
  • Bridge for string, int, float, bool, array types
  • Proof of concept with one extension (e.g., mbstring or curl)
  • --ext flag to specify extension libraries at compile time
  • Documentation: how to bridge a PHP extension

v0.29.x — WebAssembly target

  • WASM codegen backend
  • .wat / .wasm emission
  • WASI support for I/O
  • NPM package generation

Deferred ideas

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.

Future 1.0 perspective

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