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: 2 additions & 0 deletions docs/internals/the-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Things that have a value:
| `BufferNew { element_type, len }` | `buffer_new<int>(256)` | Compiler extension for contiguous hot-path buffers |
| `MagicConstant(MagicConstant)` | `__DIR__`, `__CLASS__` | Parsed from case-insensitive magic-constant tokens. `__LINE__` is lowered immediately to `IntLiteral`; the remaining magic constants are lowered by `src/magic_constants.rs` before type checking. |
| `ClassConstant { receiver }` | `MyClass::class`, `\App\C::class`, `self::class`, `parent::class`, `static::class` | The PHP `::class` reflection literal. Codegen lowers it to a string literal carrying the fully-qualified class name. `static::class` follows late static binding. |
| `ObjectClassName { object }` | `$object::class`, `make_object()::class` | Runtime `::class` lookup for an object-valued expression. The object expression remains structural so later passes evaluate it once and read its concrete runtime class. |
| `ScopedConstantAccess { receiver, name }` | `MyClass::LIMIT`, `self::DEFAULT_SIZE` | User-declared class constant access through `::`; later phases resolve the receiver and constant metadata. |

### Statements (`Stmt`)
Expand Down Expand Up @@ -391,6 +392,7 @@ Before looking for infix operators, the parser handles **prefix** constructs —
| `new` + `$var` + `(` | Parse dynamic object instantiation → `NewDynamic` (class named by a runtime variable) |
| `new` + `self` / `static` / `parent` + `(` | Parse scoped object instantiation → `NewScopedObject` |
| `<receiver>::class` | Parse `MyClass::class`, `\App\C::class`, `self::class`, `parent::class`, `static::class` → `ClassConstant` |
| `<object-expression>::class` | Parse `$object::class`, `make_object()::class` → `ObjectClassName` |
| `$this` | Return `This` node |
| `...` + expr | Parse spread/unpack → `Spread` |
| `ptr_cast` + `<Type>` + `(` | Parse pointer cast syntax → `PtrCast` |
Expand Down
13 changes: 11 additions & 2 deletions docs/php/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,9 @@ Called with `::`, no `$this`.

## Class name reflection (`::class`)

`::class` returns the fully-qualified class name as a string at compile time.
`::class` returns a fully-qualified class name as a string. Named and scoped
receivers are resolved without constructing an object; an object-valued receiver
reads the concrete class from the object at runtime.

```php
<?php
Expand All @@ -551,11 +553,18 @@ class Logger {
}
echo Logger::class; // "App\Logger"
echo \App\Logger::class; // "App\Logger"

$logger = new Logger("audit");
echo $logger::class; // "App\Logger"
```

Supported receivers: `Class::class`, `\Vendor\Class::class`, `self::class`, `parent::class`, `static::class`.
Supported receivers: `Class::class`, `\Vendor\Class::class`, `self::class`,
`parent::class`, `static::class`, and statically known object-valued expressions
such as `$object::class` or `make_object()::class`.

`static::class` follows PHP late static binding and resolves to the called class.
`$object::class` follows the object's concrete runtime class, including when its
static type is a parent class. The receiver expression is evaluated exactly once.
For named receivers, elephc preserves PHP's written/imported spelling for the
`::class` string while still using case-insensitive class lookup for executable
operations such as `new`, `instanceof`, static method calls, and static property
Expand Down
2 changes: 2 additions & 0 deletions examples/static-classes/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public static function default_logger(): Logger {

$named = new Logger("audit");
echo "Named logger: " . $named->describe() . "\n";
// Object receivers resolve the concrete runtime class, like get_class($named).
echo "Named logger runtime class: " . $named::class . "\n";

// Inheritance + parent::class
class Base {
Expand Down
3 changes: 2 additions & 1 deletion src/autoload/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ fn collect_refs_expr(expr: &Expr, out: &mut HashSet<String>) {
}
}
ExprKind::PropertyAccess { object, .. }
| ExprKind::NullsafePropertyAccess { object, .. } => collect_refs_expr(object, out),
| ExprKind::NullsafePropertyAccess { object, .. }
| ExprKind::ObjectClassName { object } => collect_refs_expr(object, out),
ExprKind::MethodCall { object, args, .. }
| ExprKind::NullsafeMethodCall { object, args, .. } => {
collect_refs_expr(object, out);
Expand Down
3 changes: 3 additions & 0 deletions src/codegen_support/program_usage/required_classes/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,9 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet<String>
names.insert(name.as_str().to_string());
}
}
ExprKind::ObjectClassName { object } => {
collect_required_class_names_in_expr(object, names);
}
ExprKind::ScopedConstantAccess { receiver, .. } => {
if let crate::parser::ast::StaticReceiver::Named(name) = receiver {
names.insert(name.as_str().to_string());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool {
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. }
| ExprKind::NamedArg { value: expr, .. }
| ExprKind::BufferNew { len: expr, .. } => expr_has_dynamic_instanceof(expr),
| ExprKind::BufferNew { len: expr, .. }
| ExprKind::ObjectClassName { object: expr } => expr_has_dynamic_instanceof(expr),
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default } => {
expr_has_dynamic_instanceof(value) || expr_has_dynamic_instanceof(default)
Expand Down
2 changes: 2 additions & 0 deletions src/codegen_support/runtime_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ fn expr_has_regex_call(expr: &Expr) -> bool {
is_regex_builtin_name(name.as_str())
}
ExprKind::FirstClassCallable(CallableTarget::StaticMethod { .. }) => false,
ExprKind::ObjectClassName { object } => expr_has_regex_call(object),
ExprKind::StaticPropertyAccess { receiver, .. }
| ExprKind::ClassConstant { receiver }
| ExprKind::ScopedConstantAccess { receiver, .. } => {
Expand Down Expand Up @@ -794,6 +795,7 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool {
expr_needs_descriptor_invoker(object)
}
ExprKind::FirstClassCallable(_) => false,
ExprKind::ObjectClassName { object } => expr_needs_descriptor_invoker(object),
ExprKind::StaticPropertyAccess { .. }
| ExprKind::ClassConstant { .. }
| ExprKind::ScopedConstantAccess { .. } => false,
Expand Down
2 changes: 2 additions & 0 deletions src/eval_aot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,7 @@ fn expr_fallback_reason(expr: &Expr) -> Option<EvalAotFallbackReason> {
| ExprKind::NullsafeMethodCall { .. }
| ExprKind::StaticMethodCall { .. }
| ExprKind::ClassConstant { .. }
| ExprKind::ObjectClassName { .. }
| ExprKind::ScopedConstantAccess { .. }
| ExprKind::This => Some(EvalAotFallbackReason::ObjectOrMemberAccess),
ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) | ExprKind::Spread(_) => {
Expand Down Expand Up @@ -2566,6 +2567,7 @@ fn collect_expr_scope_access(expr: &Expr, access: &mut EvalScopeAccess) {
| ExprKind::ClassConstant { .. }
| ExprKind::ScopedConstantAccess { .. }
| ExprKind::MagicConstant(_) => {}
ExprKind::ObjectClassName { object } => collect_expr_scope_access(object, access),
ExprKind::Variable(name)
| ExprKind::PreIncrement(name)
| ExprKind::PostIncrement(name)
Expand Down
1 change: 1 addition & 0 deletions src/image_prelude/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ fn expr_refs_image(expr: &Expr) -> bool {
}
ExprKind::ClassConstant { receiver }
| ExprKind::ScopedConstantAccess { receiver, .. } => receiver_refs_image(receiver),
ExprKind::ObjectClassName { object } => expr_refs_image(object),
ExprKind::NewScopedObject { receiver, args } => {
receiver_refs_image(receiver) || args.iter().any(expr_refs_image)
}
Expand Down
24 changes: 23 additions & 1 deletion src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe
ExprKind::PtrCast { target_type, expr: inner } => lower_ptr_cast(ctx, target_type, inner, expr),
ExprKind::BufferNew { element_type, len } => lower_buffer_new(ctx, element_type, len, expr),
ExprKind::ClassConstant { receiver } => lower_class_constant(ctx, receiver, expr),
ExprKind::ObjectClassName { object } => lower_object_class_name(ctx, object, expr),
ExprKind::ScopedConstantAccess { receiver, name } => {
lower_scoped_constant(ctx, receiver, name, expr)
}
Expand Down Expand Up @@ -713,7 +714,10 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool {
| ExprKind::NamedArg { value: inner, .. }
| ExprKind::Spread(inner)
| ExprKind::PtrCast { expr: inner, .. }
| ExprKind::BufferNew { len: inner, .. } => expr_can_reset_concat_storage(inner),
| ExprKind::BufferNew { len: inner, .. }
| ExprKind::ObjectClassName { object: inner } => {
expr_can_reset_concat_storage(inner)
}
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default } => {
expr_can_reset_concat_storage(value) || expr_can_reset_concat_storage(default)
Expand Down Expand Up @@ -9421,6 +9425,7 @@ fn expr_contains_eval_call(expr: &Expr) -> bool {
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. }
| ExprKind::BufferNew { len: expr, .. }
| ExprKind::ObjectClassName { object: expr }
| ExprKind::YieldFrom(expr) => expr_contains_eval_call(expr),
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default }
Expand Down Expand Up @@ -14433,6 +14438,23 @@ fn lower_class_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticRece
)
}

/// Lowers an object-valued `::class` receiver through the runtime class-name lookup.
fn lower_object_class_name(
ctx: &mut LoweringContext<'_, '_>,
object: &Expr,
expr: &Expr,
) -> LoweredValue {
let object = lower_expr(ctx, object);
emit_builtin_call_value(
ctx,
"get_class",
vec![object.value],
PhpType::Str,
expr.span,
None,
)
}

/// Lowers a scoped constant read.
fn lower_scoped_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticReceiver, name: &str, expr: &Expr) -> LoweredValue {
let class_name = scoped_constant_receiver_name(ctx, receiver);
Expand Down
1 change: 1 addition & 0 deletions src/ir_lower/tests/exhaustive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ fn lowers_every_expr_variant_smoke() {
expr(ExprKind::ClassConstant { receiver: StaticReceiver::Self_ }),
expr(ExprKind::ClassConstant { receiver: StaticReceiver::Static }),
expr(ExprKind::ClassConstant { receiver: StaticReceiver::Parent }),
expr(ExprKind::ObjectClassName { object: Box::new(object.clone()) }),
expr(ExprKind::ScopedConstantAccess { receiver: StaticReceiver::Named(name("C")), name: "K".to_string() }),
expr(ExprKind::NewScopedObject { receiver: StaticReceiver::Named(name("C")), args: Vec::new() }),
expr(ExprKind::MagicConstant(MagicConstant::File)),
Expand Down
1 change: 1 addition & 0 deletions src/list_id_prelude/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ fn expr_refs_listid(expr: &Expr) -> bool {
ExprKind::StaticPropertyAccess { .. } => false,
ExprKind::BufferNew { len, .. } => expr_refs_listid(len),
ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => false,
ExprKind::ObjectClassName { object } => expr_refs_listid(object),
ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_refs_listid),
ExprKind::Yield { key, value } => {
key.as_deref().is_some_and(expr_refs_listid)
Expand Down
3 changes: 3 additions & 0 deletions src/magic_constants/walker/exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ pub(super) fn walk_expr<P: Pass>(expr: Expr, pass: &mut P) -> Expr {
len: Box::new(walk_expr(*len, pass)),
},
ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver },
ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName {
object: Box::new(walk_expr(*object, pass)),
},
ExprKind::ScopedConstantAccess { receiver, name } => {
ExprKind::ScopedConstantAccess { receiver, name }
}
Expand Down
3 changes: 3 additions & 0 deletions src/name_resolver/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ pub(super) fn resolve_expr(
_ => receiver.clone(),
},
},
ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName {
object: Box::new(resolve_expr(object, current_namespace, imports, symbols)),
},
ExprKind::ScopedConstantAccess { receiver, name } => ExprKind::ScopedConstantAccess {
receiver: match receiver {
StaticReceiver::Named(name) => StaticReceiver::Named(resolved_name(
Expand Down
3 changes: 3 additions & 0 deletions src/optimize/control/prune/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr {
len: Box::new(prune_expr(*len)),
},
ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver },
ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName {
object: Box::new(prune_expr(*object)),
},
ExprKind::ScopedConstantAccess { receiver, name } => {
ExprKind::ScopedConstantAccess { receiver, name }
}
Expand Down
1 change: 1 addition & 0 deletions src/optimize/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect {
ExprKind::FirstClassCallable(target) => callable_target_effect(target),
ExprKind::BufferNew { len, .. } => expr_effect(len).with_side_effects(),
ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => Effect::PURE,
ExprKind::ObjectClassName { object } => expr_effect(object),
ExprKind::NewScopedObject { args, .. } => combine_effects(args.iter().map(expr_effect))
.with_side_effects()
.with_may_throw(),
Expand Down
3 changes: 3 additions & 0 deletions src/optimize/fold/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr {
len: Box::new(fold_expr(*len)),
},
ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver },
ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName {
object: Box::new(fold_expr(*object)),
},
ExprKind::ScopedConstantAccess { receiver, name } => {
ExprKind::ScopedConstantAccess { receiver, name }
}
Expand Down
3 changes: 3 additions & 0 deletions src/optimize/propagate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr {
len: Box::new(propagate_expr(*len, env)),
},
ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver },
ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName {
object: Box::new(propagate_expr(*object, env)),
},
ExprKind::ScopedConstantAccess { receiver, name } => {
ExprKind::ScopedConstantAccess { receiver, name }
}
Expand Down
1 change: 1 addition & 0 deletions src/optimize/propagate/invalidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation {
| ExprKind::This
| ExprKind::ClassConstant { .. }
| ExprKind::ScopedConstantAccess { .. } => Invalidation::none(),
ExprKind::ObjectClassName { object } => expr_invalidation(object),
// Creating a closure executes nothing, but its by-ref captures alias
// the outer variables from this point on: any existing fact for them
// must die here (the volatility ledger only blocks *future* facts).
Expand Down
6 changes: 6 additions & 0 deletions src/parser/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ pub enum ExprKind {
ClassConstant {
receiver: StaticReceiver,
},
/// `$object::class` or another object-valued expression followed by `::class`.
/// Unlike `ClassConstant`, this resolves the concrete runtime class of the
/// evaluated object instead of naming a compile-time static receiver.
ObjectClassName {
object: Box<Expr>,
},
/// Access to a user-declared class constant: `MyClass::FOO`,
/// `self::FOO`, `parent::FOO`, `static::FOO`. Resolved at type-check
/// time by looking up the constant in the receiver's class info.
Expand Down
11 changes: 9 additions & 2 deletions src/parser/expr/assignment_targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe
| ExprKind::Cast { expr: value, .. }
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::NamedArg { value, .. }
| ExprKind::Spread(value) => collect_assignment_target_dependencies(value, dependencies),
| ExprKind::Spread(value)
| ExprKind::ObjectClassName { object: value } => {
collect_assignment_target_dependencies(value, dependencies)
}
ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => {
collect_assignment_target_dependencies(value, dependencies);
collect_assignment_target_dependencies(default, dependencies);
Expand Down Expand Up @@ -483,7 +486,10 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet<String>) -> boo
| ExprKind::Cast { expr: value, .. }
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::NamedArg { value, .. }
| ExprKind::Spread(value) => expr_may_write_dependency(value, dependencies),
| ExprKind::Spread(value)
| ExprKind::ObjectClassName { object: value } => {
expr_may_write_dependency(value, dependencies)
}
ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => {
expr_may_write_dependency(value, dependencies)
|| expr_may_write_dependency(default, dependencies)
Expand Down Expand Up @@ -695,6 +701,7 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool {
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::NamedArg { value, .. }
| ExprKind::Spread(value)
| ExprKind::ObjectClassName { object: value }
| ExprKind::YieldFrom(value) => expr_contains_equivalent(value, needle),
ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => {
expr_contains_equivalent(value, needle) || expr_contains_equivalent(default, needle)
Expand Down
11 changes: 11 additions & 0 deletions src/parser/expr/pratt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ fn parse_expr_bp_inner(
// `call_user_func([$cls, $method], ...args)`, reusing the runtime dispatch path.
let span = tokens[*pos].1;
*pos += 1; // consume '::'
if matches!(tokens.get(*pos).map(|(token, _)| token), Some(Token::Class)) {
*pos += 1;
let span = crate::parser::expr::span_through_prev_token(tokens, *pos, span);
lhs = Expr::new(
ExprKind::ObjectClassName {
object: Box::new(lhs),
},
span,
);
continue;
}
let member = match tokens.get(*pos).map(|(token, s)| (token.clone(), *s)) {
Some((Token::Identifier(name), name_span)) => {
*pos += 1;
Expand Down
1 change: 1 addition & 0 deletions src/parser/expr/prefix_complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ fn collect_arrow_expr_captures(
| ExprKind::Spread(inner)
| ExprKind::PtrCast { expr: inner, .. }
| ExprKind::Cast { expr: inner, .. }
| ExprKind::ObjectClassName { object: inner }
| ExprKind::YieldFrom(inner) => collect_arrow_expr_captures(inner, bound, seen, captures),
ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => {
collect_arrow_expr_captures(value, bound, seen, captures);
Expand Down
1 change: 1 addition & 0 deletions src/pdo_prelude/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ fn expr_refs_pdo(expr: &Expr) -> bool {
}
ExprKind::ClassConstant { receiver }
| ExprKind::ScopedConstantAccess { receiver, .. } => receiver_refs_pdo(receiver),
ExprKind::ObjectClassName { object } => expr_refs_pdo(object),
ExprKind::NewScopedObject { receiver, args } => {
receiver_refs_pdo(receiver) || args.iter().any(expr_refs_pdo)
}
Expand Down
1 change: 1 addition & 0 deletions src/resolver/contains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ fn expr_has_includes(expr: &Expr) -> bool {
ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => {
expr_has_includes(object)
}
ExprKind::ObjectClassName { object } => expr_has_includes(object),
ExprKind::StringLiteral(_)
| ExprKind::IntLiteral(_)
| ExprKind::FloatLiteral(_)
Expand Down
3 changes: 3 additions & 0 deletions src/resolver/discovery/exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ pub(super) fn discover_expr(
ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. }) => {
discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?;
}
ExprKind::ObjectClassName { object } => {
discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?;
}
ExprKind::StringLiteral(_)
| ExprKind::IntLiteral(_)
| ExprKind::FloatLiteral(_)
Expand Down
Loading
Loading