Skip to content
Merged
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,8 @@ runtime helpers, and standard-library surfaces.
- [x] 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
- [x] 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.
- [x] 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
- [X] 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 — continue with PHP's uninitialized typed-property state, integer overflow promotion, broader loose-comparison semantics, and future warning/notice sites as they are added
- [x] 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
- [x] 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.
- [x] Broader date and regex PHP parity — expand `strtotime()` relative formats with `a/an <unit>` article offsets and add `preg_replace()` capture backreference expansion (`$0`..`$9`, `\0`..`\9`) over the POSIX bridge (JSON parity now closed: see v0.8.x base + v0.20.x polish)
- [x] 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.
- [x] 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.
Expand Down
4 changes: 2 additions & 2 deletions docs/php/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class User {
}
```

Property type declarations are checked at compile time for both instance and static properties. Defaults and later assignments must be compatible with the declared type, including constructor assignments through untyped parameters. Nullable shorthand (`?T`) and union storage use the compiler's boxed mixed representation internally. `void` and `callable` property types are rejected.
Property type declarations are checked at compile time for both instance and static properties. Defaults and later assignments must be compatible with the declared type, including constructor assignments through untyped parameters. Typed properties without an explicit default start in PHP's uninitialized state; reading an instance or static property before the first assignment is a fatal runtime error, while assigning values such as `0`, `false`, `""`, or `null` to compatible nullable storage initializes the slot normally. Nullable shorthand (`?T`) and union storage use the compiler's boxed mixed representation internally. `void` and `callable` property types are rejected.

### Property redeclaration

Expand Down Expand Up @@ -220,7 +220,7 @@ Counter::$count = 5;
echo Counter::bump(); // 6
```

Supported receivers are `ClassName::$prop`, `self::$prop`, `parent::$prop`, and `static::$prop`. Static property visibility and declared types are checked at compile time. Inherited static properties share the declaring class storage until a subclass redeclares the property. Redeclarations follow PHP rules: non-private inherited properties keep invariant declared types, cannot reduce visibility, and cannot override `final` properties. Private static properties redeclared in subclasses are independent slots; `static::$prop` is still late-bound and reports a fatal runtime error if the current method scope cannot access the matched private slot.
Supported receivers are `ClassName::$prop`, `self::$prop`, `parent::$prop`, and `static::$prop`. Static property visibility and declared types are checked at compile time. Typed static properties without defaults use the same uninitialized-read fatal as typed instance properties. Inherited static properties share the declaring class storage until a subclass redeclares the property. Redeclarations follow PHP rules: non-private inherited properties keep invariant declared types, cannot reduce visibility, and cannot override `final` properties. Private static properties redeclared in subclasses are independent slots; `static::$prop` is still late-bound and reports a fatal runtime error if the current method scope cannot access the matched private slot.

Static properties in elephc, like in PHP, are always mutable — even on a `readonly class`. PHP's `readonly` modifier only constrains instance properties; declaring `public readonly static` is a compile error in both PHP and elephc.

Expand Down
10 changes: 5 additions & 5 deletions docs/php/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ sidebar:

| Operator | Example | Notes |
|---|---|---|
| `+` | `$a + $b` | Numeric addition, or PHP array union when both operands are arrays |
| `-` | `$a - $b` | Subtraction |
| `*` | `$a * $b` | Multiplication |
| `+` | `$a + $b` | Numeric addition, or PHP array union when both operands are arrays. Integer overflow promotes to `double`. |
| `-` | `$a - $b` | Subtraction. Integer overflow promotes to `double`. |
| `*` | `$a * $b` | Multiplication. Integer overflow promotes to `double`. |
| `/` | `$a / $b` | Division (always returns float) |
| `%` | `$a % $b` | Modulo |
| `**` | `$a ** $b` | Exponentiation (right-associative, returns float) |
Expand All @@ -21,8 +21,8 @@ sidebar:

| Operator | Example | Notes |
|---|---|---|
| `==` | `$a == $b` | Loose equality (cross-type: coerces to int) |
| `!=` | `$a != $b` | Inequality |
| `==` | `$a == $b` | Loose equality using PHP-style scalar coercions for bool, null, numeric strings, and non-numeric strings |
| `!=` | `$a != $b` | Loose inequality using the same scalar coercions as `==` |
| `===` | `$a === $b` | Strict equality (type and value) |
| `!==` | `$a !== $b` | Strict inequality |
| `<` | `$a < $b` | Less than |
Expand Down
5 changes: 2 additions & 3 deletions docs/php/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,9 @@ Aliases: `(integer)`, `(double)`, `(real)`, `(boolean)`.
### Known incompatibilities with PHP

- `$argv[0]` returns the compiled binary path, not the `.php` file path.
- Integer overflow wraps instead of promoting to float.
- Loose comparison (`==`) between different types coerces both sides to integer.
- Integer `+`, `-`, and `*` overflow promotes to `double` for both constant-folded and non-folded runtime scalar arithmetic.
- Scalar loose comparison (`==`, `!=`) follows PHP-style bool truthiness, null-vs-empty-string, numeric-string, and non-numeric string byte-comparison rules for constant-folded literals and non-folded runtime scalar operands.
- `??=` is checked against typed assignment storage for variables, object properties, static properties, and non-append array elements. For concrete local variable types, the fallback must keep the same type or be a literal `null`.
- elephc does not model PHP's uninitialized typed-property state; property slots without explicit defaults start from the compiler's existing zero/null-like object-slot initialization until assigned.
- Plain array numeric casts (`(int)$array`, `(float)$array`) follow elephc's existing array cast semantics (return the element count rather than PHP's `0`/`1`). Direct `iterable` numeric casts use PHP's empty/non-empty `0`/`1` semantics.
- `FiberError` is currently modeled as an `Exception` subclass in elephc; PHP models `FiberError` under `Error`.

Expand Down
27 changes: 24 additions & 3 deletions src/codegen/builtins/pointers/ptr_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
use crate::codegen::context::Context;
use crate::codegen::data_section::DataSection;
use crate::codegen::emit::Emitter;
use crate::codegen::expr::emit_expr;
use crate::codegen::expr::{coerce_result_to_type, emit_expr, expr_result_heap_ownership};
use crate::codegen::{abi, platform::Arch};
use crate::parser::ast::Expr;
use crate::codegen::context::HeapOwnership;
use crate::parser::ast::{BinOp, Expr, ExprKind};
use crate::types::PhpType;

pub fn emit(
Expand All @@ -30,7 +31,27 @@ pub fn emit(
abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the validated destination pointer while the stored value expression is evaluated

// -- evaluate value to write --
emit_expr(&args[1], emitter, ctx, data);
let value_ty = emit_expr(&args[1], emitter, ctx, data);
let release_mixed_after_coerce = matches!(value_ty, PhpType::Mixed | PhpType::Union(_))
&& (expr_result_heap_ownership(&args[1]) == HeapOwnership::Owned
|| matches!(
args[1].kind,
ExprKind::BinaryOp {
op: BinOp::Add | BinOp::Sub | BinOp::Mul,
..
}
));
if release_mixed_after_coerce {
abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the boxed Mixed value so it can be released after integer coercion
}
coerce_result_to_type(emitter, ctx, data, &value_ty, &PhpType::Int);
if release_mixed_after_coerce {
abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); // preserve the coerced integer payload while releasing the temporary Mixed box
abi::emit_load_temporary_stack_slot(emitter, abi::int_result_reg(emitter), 16);
abi::emit_decref_if_refcounted(emitter, &PhpType::Mixed);
abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); // restore the coerced integer payload after temporary Mixed cleanup
abi::emit_release_temporary_stack(emitter, 16);
}

// -- store value at pointer address --
match emitter.target.arch {
Expand Down
22 changes: 21 additions & 1 deletion src/codegen/driver_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use super::platform::{Arch, Target};
use super::runtime;

const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048;
pub(crate) const UNINITIALIZED_TYPED_PROPERTY_SENTINEL: i64 = 0x7fff_ffff_ffff_fffd;

pub(super) fn emit_write_literal_stderr(emitter: &mut Emitter, label: &str, len: usize) {
match emitter.target.arch {
Expand Down Expand Up @@ -139,6 +140,7 @@ pub(super) fn emit_static_property_initializers(
ctx: &mut Context,
) {
let mut initializers = Vec::new();
let mut uninitialized_static_properties = Vec::new();
let mut sorted_classes: Vec<(&String, &ClassInfo)> = ctx.classes.iter().collect();
sorted_classes.sort_by_key(|(class_name, _)| class_name.as_str());
for (class_name, class_info) in sorted_classes {
Expand All @@ -151,7 +153,11 @@ pub(super) fn emit_static_property_initializers(
if declaring_class != class_name {
continue;
}
let Some(default_expr) = class_info.static_defaults.get(index).cloned().flatten() else {
let default_expr = class_info.static_defaults.get(index).cloned().flatten();
if default_expr.is_none() && class_info.declared_static_properties.contains(property_name) {
uninitialized_static_properties.push((class_name.clone(), property_name.clone()));
}
let Some(default_expr) = default_expr else {
continue;
};
let declared = class_info.declared_static_properties.contains(property_name);
Expand All @@ -165,6 +171,17 @@ pub(super) fn emit_static_property_initializers(
}
}

for (class_name, property_name) in uninitialized_static_properties {
emitter.comment(&format!(
"mark static property {}::${} uninitialized",
class_name, property_name
));
let marker_reg = abi::int_result_reg(emitter);
abi::emit_load_int_immediate(emitter, marker_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL);
let symbol = crate::names::static_property_symbol(&class_name, &property_name);
abi::emit_store_reg_to_symbol(emitter, marker_reg, &symbol, 8);
}

for (class_name, property_name, prop_ty, default_expr, declared) in initializers {
emitter.comment(&format!(
"initialize static property {}::${}",
Expand All @@ -179,6 +196,9 @@ pub(super) fn emit_static_property_initializers(
};
let symbol = crate::names::static_property_symbol(&class_name, &property_name);
abi::emit_store_result_to_symbol(emitter, &symbol, &store_ty, false);
if !matches!(store_ty.codegen_repr(), PhpType::Str) {
abi::emit_store_zero_to_symbol(emitter, &symbol, 8);
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/codegen/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use super::emit::Emitter;
use crate::parser::ast::{BinOp, Expr, ExprKind};
use crate::types::PhpType;

pub(crate) use helpers::coerce_result_to_type;
pub(crate) use helpers::{can_coerce_result_to_type, coerce_result_to_type};
pub(crate) use objects::{emit_method_call_with_pushed_args, push_magic_property_name_arg};
pub(crate) use ownership::expr_result_heap_ownership;
pub use coerce::{coerce_null_to_zero, coerce_to_string, coerce_to_truthiness};
Expand Down Expand Up @@ -218,7 +218,7 @@ pub fn emit_expr(
objects::emit_nullsafe_property_access(object, property, emitter, ctx, data)
}
ExprKind::StaticPropertyAccess { receiver, property } => {
objects::emit_static_property_access(receiver, property, emitter, ctx)
objects::emit_static_property_access(receiver, property, emitter, ctx, data)
}
ExprKind::MethodCall {
object,
Expand Down
Loading
Loading