Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ The full list of supported constructs, operators, and control structures is in t
<details>
<summary>Show the construct highlights</summary>

- **OOP**: classes, abstract/final classes, typed/final/static properties and methods, PHP-style static property redeclarations, direct static array property writes, constructor property promotion, interfaces, `instanceof`, traits, enums, PHP 8 declaration attributes, limited attribute reflection (`ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()`, `ReflectionAttribute::newInstance()`), `readonly`, static/instance methods, case-insensitive class/interface/trait and method lookup, `self::`/`parent::`/`static::`, `::class` reflection (including `$object::class` on object expressions, returning the receiver's runtime class), class constants including PHP 8.3 typed class constants (exposed via `ReflectionClassConstant::hasType()`/`getType()`), `new self()` / `new static()` / `new parent()`, magic methods (`__toString`, `__get`, `__set`, `__isset`, `__unset`, `__call`, `__invoke`, `__clone`, `__destruct`), `clone`, `get_object_vars()` and `(array)` casts on objects
- **OOP**: classes, abstract/final classes, typed/final/static properties and methods, PHP-style static property redeclarations, direct static array property writes, constructor property promotion, interfaces, `instanceof`, traits, enums, PHP 8 declaration attributes, limited attribute reflection (`ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()`, `ReflectionAttribute::newInstance()`), `readonly`, static/instance methods, case-insensitive class/interface/trait and method lookup, `self::`/`parent::`/`static::`, `::class` reflection (including `$object::class` on object expressions, returning the receiver's runtime class), class constants including PHP 8.3 typed class constants (exposed via `ReflectionClassConstant::hasType()`/`getType()`), `new self()` / `new static()` / `new parent()`, magic methods (`__toString`, `__get`, `__set`, `__isset`, `__unset`, `__call`, `__invoke`, `__clone`, `__destruct`), `clone`, `get_object_vars()`, `(array)` casts on objects, and `(object)` casts (array keys become properties, `null` becomes an empty `stdClass`, a scalar lands on `scalar`, and an object source is returned unchanged)
- **Functions**: case-insensitive user and built-in function calls, default parameters, variadic/spread, pass by reference, named arguments, global variables, static locals, first-class callables, closures, arrow functions, static closures (`static function () { }`, `static fn () => ...`)
- **Generators**: generator functions and closures, `yield`, key/value yields, `yield from`, `Generator::send()`, `throw()`, `getReturn()`, and `foreach` over `Iterator` / `IteratorAggregate`
- **Fibers**: `Fiber`, `FiberError`, `Fiber::suspend()`, `Fiber::getCurrent()`, `start()`, `resume()`, `throw()`, `getReturn()`, state predicates, closure captures, guarded native stacks, and target-aware context switching on the three executable/release hosts (macOS ARM64, Linux ARM64, and Linux x86_64); the iOS compile targets are library-only and do not run Fibers
Expand Down
9 changes: 9 additions & 0 deletions docs/php/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,17 @@ $b = (bool)0; // false
$a = (array)42; // [42]
$o = (array)$obj; // property name => value hash, with PHP's visibility-mangled keys
$m = (array)$mixed; // dispatches on the runtime tag: arrays pass through, scalars wrap, objects project
$p = (object)['k' => 1]; // stdClass with a `k` property
$q = (object)42; // stdClass with a `scalar` property
```

`(object)` converts an array to a `stdClass` whose property names are the
array's keys rendered as strings (so `(object)['x', 'y']` has the properties
`0` and `1`, reachable as `->{'0'}`), `null` to an empty `stdClass`, and every
other non-object value to a `stdClass` carrying it in a single `scalar`
property. An object source is returned **unchanged** — `(object)$obj === $obj`
— rather than copied.

`(array)` on an object projects all of its properties into a string-keyed hash
using PHP's exact key mangling — `x` for a public property, `"\0*\0y"` for a
protected one, `"\0Class\0z"` for a private one — including the
Expand Down
3 changes: 3 additions & 0 deletions examples/object-cast/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.s
*.o
main
41 changes: 41 additions & 0 deletions examples/object-cast/main.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
// The (object) cast: turning configuration arrays into property bags.
//
// A common shape in PHP libraries is to accept an options array and hand it on as an object,
// so callers read `$options->timeout` instead of `$options['timeout']`.

function settings(string $host, int $port, bool $secure): stdClass
{
return (object) ["host" => $host, "port" => $port, "secure" => $secure];
}

$config = settings("localhost", 6432, true);

echo "host: " . $config->host . "\n";
echo "port: " . $config->port . "\n";
echo "secure: " . ($config->secure ? "yes" : "no") . "\n";

// Array keys become property names, so the object round-trips back to the same array.
$back = (array) $config;
echo "keys: " . implode(", ", array_keys($back)) . "\n";

// A non-array value lands on PHP's `scalar` property.
$wrapped = (object) "just a string";
echo "scalar: " . $wrapped->scalar . "\n";

// null becomes an empty object rather than one holding a null `scalar`.
$empty = (object) null;
echo "empty: " . count(get_object_vars($empty)) . " properties\n";

// An object is returned unchanged — the cast is the identity, not a copy.
class Endpoint
{
public function __construct(public string $url) {}
}

$endpoint = new Endpoint("https://example.test");
$same = (object) $endpoint;
$same->url = "https://changed.test";

echo "same: " . ($same === $endpoint ? "yes" : "no") . "\n";
echo "url: " . $endpoint->url . "\n";
2 changes: 1 addition & 1 deletion src/ir_lower/expr/call_arg_coercion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ fn apply_scalar_param_cast(
CastType::String => coerce_to_string_at_span(ctx, value, span),
CastType::Bool => lower_truthy_bool(ctx, value, span),
// `param_binding::scalar_param_cast` only ever reports the two total scalar casts.
CastType::Int | CastType::Float | CastType::Array => value,
CastType::Int | CastType::Float | CastType::Array | CastType::Object => value,
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/ir_lower/expr/ternary_cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ pub(super) fn lower_ternary(

/// Lowers a cast expression.
pub(super) fn lower_cast(ctx: &mut LoweringContext<'_, '_>, target: &CastType, inner: &Expr, expr: &Expr) -> LoweredValue {
if matches!(target, CastType::Object) {
return lower_object_cast(ctx, inner, expr);
}
let value = lower_expr(ctx, inner);
// Keep the original producer visible for a no-op string cast. Wrapping an
// owned string temporary in `Cast(Str)` would hide its ownership from the
Expand Down Expand Up @@ -87,6 +90,44 @@ pub(super) fn lower_cast(ctx: &mut LoweringContext<'_, '_>, target: &CastType, i
result
}

/// Lowers PHP's `(object)` cast.
///
/// An object source is returned UNCHANGED — PHP's `(object)` is the identity on an object,
/// so no copy is made and `(object) $o === $o` holds. Every other source is converted by the
/// elephc-PHP helpers `object_cast_prelude` injects, which is what keeps the conversion
/// (array keys become property names, `null` becomes an empty stdClass, a scalar becomes a
/// `scalar` property) correct on every supported target with no per-target assembly.
///
/// The source is lowered ONCE into a synthetic local, and the helper call then names that
/// local — the same rewrite `ref_place_args` uses — so the ordinary user-call path handles
/// argument lowering, the return type, and owned-temporary release, and the source's side
/// effects happen exactly once.
fn lower_object_cast(
ctx: &mut LoweringContext<'_, '_>,
inner: &Expr,
expr: &Expr,
) -> LoweredValue {
let value = lower_expr(ctx, inner);
let source_type = ctx.builder.value_php_type(value.value);
if matches!(source_type.codegen_repr(), PhpType::Object(_)) {
return value;
}
let helper = if matches!(
source_type.codegen_repr(),
PhpType::Mixed | PhpType::Union(_)
) {
crate::object_cast_prelude::DYNAMIC_CAST_HELPER
} else {
crate::object_cast_prelude::CAST_HELPER
};
let local_type = normalize_value_php_type(source_type);
let temp = ctx.declare_synthetic_php_local(local_type.clone());
ctx.store_local(&temp, value, local_type, Some(inner.span));
let argument = Expr::new(ExprKind::Variable(temp), inner.span);
let name = Name::from(helper.to_string());
lower_function_call(ctx, &name, std::slice::from_ref(&argument), expr)
Comment thread
Guikingone marked this conversation as resolved.
}

/// Releases an owning temporary when a scalar coercion cannot alias its source storage.
pub(super) fn release_coerced_source_if_owned(
ctx: &mut LoweringContext<'_, '_>,
Expand Down Expand Up @@ -145,5 +186,20 @@ pub(super) fn cast_php_type(target: &CastType, source_type: &PhpType) -> PhpType
PhpType::Mixed | PhpType::Union(_)
) => PhpType::Mixed,
CastType::Array => PhpType::Array(Box::new(PhpType::Mixed)),
// Mirrors the checker's `(object)` arms in
// `types::checker::inference::expr::basic`: identity on an object, `mixed` for a
// runtime-typed source that may already hold an unrelated class, stdClass otherwise.
CastType::Object if matches!(source_type.codegen_repr(), PhpType::Object(_)) => {
source_type.clone()
}
CastType::Object
if matches!(
source_type.codegen_repr(),
PhpType::Mixed | PhpType::Union(_)
) =>
{
PhpType::Mixed
}
CastType::Object => PhpType::Object("stdClass".to_string()),
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub(crate) mod numeric_string;
pub mod opcache;
/// `opcache_get_configuration()` standard-library prelude injection.
pub mod opcache_prelude;
/// PHP `(object)` cast standard-library prelude injection.
pub mod object_cast_prelude;
/// Optimizer passes.
pub mod optimize;
/// Parser for PHP syntax.
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod name_resolver;
mod native_deps;
mod names;
mod numeric_string;
mod object_cast_prelude;
mod opcache;
mod opcache_prelude;
mod optimize;
Expand Down
Loading
Loading