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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Releases are listed newest first.

## [Unreleased]
- Added experimental PHP `eval()` support across macOS ARM64, Linux ARM64, and Linux x86_64. 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. Within the supported eval subset, the fallback preserves caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, ownership/COW behavior, and PHP-visible diagnostics without requiring PHP or the Zend Engine. Bridge linking is automatic when required and can be forced with `--with-eval`; generated builtin documentation now reports AOT and eval availability separately.
- Added PHP 8.3 typed class constants for classes, interfaces, traits, and enums. Declared types are enforced for initializers and inherited overrides, including covariant narrowing, and are exposed through `ReflectionClassConstant::hasType()` and `getType()`; untyped constants and enum cases retain PHP-compatible reflection defaults.
- Fixed `--web --max-requests` crash-loop accounting (issue #516): workers that exit after a planned request-quota recycle now use a dedicated status that the master excludes from startup-death streaks, so sustained traffic with a low quota keeps respawning workers instead of shutting down the server; genuine setup failures, other exit codes, and signal deaths still feed the guard.
- Fixed nullable chained array reads (issue #525): consuming a value from a `?array` receiver now releases the one-shot hidden owned Mixed temporary created by nullable access, on both null and non-null paths, without invalidating the extracted result or double-releasing repeated reads.
- Fixed owned boxed Mixed temporaries leaking after string conversion (issue #527): explicit `(string)` casts plus implicit concatenation and interpolation now release detached Mixed sources, including by-value `foreach` element reads and non-null unions represented as Mixed; borrowed, persistent, moved, and non-heap values remain release no-ops.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ pub(super) fn eval_reflection_owner_object_with_members(
backing_value_cell,
constructor,
)?;
if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT {
let has_type = values.bool_value(type_metadata.is_some())?;
let type_value = match type_metadata {
Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?,
None => values.null()?,
};
eval_reflection_with_declaring_class_scope(
"ReflectionClassConstant",
context,
|_| -> Result<(), EvalStatus> {
values.property_set(object, "__has_type", has_type)?;
values.property_set(object, "__type", type_value)
},
)?;
}
if matches!(
owner_kind,
EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM
Expand Down
14 changes: 8 additions & 6 deletions docs/php/classes.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples/classes/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Classes — constructors, methods, properties

class Counter {
const STEP = 1;
const TRIPLE_STEP = self::STEP * 3;
const int STEP = 1;
const int TRIPLE_STEP = self::STEP * 3;

public $count;

Expand Down
89 changes: 86 additions & 3 deletions src/codegen/lower_inst/objects/reflection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ struct ReflectionClassConstantMetadata {
attr_names: Vec<String>,
attr_args: Vec<Option<Vec<AttrArgEntry>>>,
value: ReflectionConstantValue,
type_metadata: Option<ReflectionParameterTypeMetadata>,
visibility: Visibility,
is_final: bool,
}
Expand Down Expand Up @@ -828,6 +829,15 @@ fn emit_reflection_owner_object(
emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__value")?;
}
}
if class_name == "ReflectionClassConstant" {
emit_reflection_owner_bool_property(
ctx,
class_name,
"__has_type",
metadata.type_metadata.is_some(),
)?;
emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?;
}
if class_name == "ReflectionEnumBackedCase" {
if let Some(value) = &metadata.backing_value {
abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter));
Expand Down Expand Up @@ -1960,7 +1970,7 @@ fn reflection_class_constant_owner_metadata(
backing_value: None,
is_enum_case: false,
parameter_members: Vec::new(),
type_metadata: None,
type_metadata: metadata.type_metadata,
property_default_value: None,
required_parameter_count: 0,
is_deprecated: false,
Expand Down Expand Up @@ -2008,6 +2018,10 @@ fn reflection_class_constant_lookup(
.cloned()
.unwrap_or_default(),
value,
type_metadata: info
.constant_types
.get(constant_name)
.and_then(reflection_declared_type_metadata),
visibility: info
.constant_visibilities
.get(constant_name)
Expand Down Expand Up @@ -2050,6 +2064,12 @@ fn reflection_class_constant_lookup(
attr_names: Vec::new(),
attr_args: Vec::new(),
value,
type_metadata: ctx
.module
.declared_trait_constant_types
.get(trait_name)
.and_then(|types| types.get(constant_name))
.and_then(reflection_declared_type_metadata),
visibility: ctx
.module
.declared_trait_constant_visibilities
Expand Down Expand Up @@ -2092,6 +2112,10 @@ fn reflection_interface_class_constant_lookup(
attr_names: Vec::new(),
attr_args: Vec::new(),
value,
type_metadata: info
.constant_types
.get(constant_name)
.and_then(reflection_declared_type_metadata),
visibility: Visibility::Public,
is_final,
}))
Expand Down Expand Up @@ -2643,6 +2667,7 @@ fn reflection_class_constant_reflection_members(
enum_name: class_name.to_string(),
case_name: case.name.clone(),
},
None,
Visibility::Public,
false,
true,
Expand Down Expand Up @@ -2677,6 +2702,10 @@ fn reflection_class_constant_reflection_members(
.cloned()
.unwrap_or_default(),
value,
current_info
.constant_types
.get(constant_name)
.and_then(reflection_declared_type_metadata),
current_info
.constant_visibilities
.get(constant_name)
Expand Down Expand Up @@ -2780,6 +2809,10 @@ fn collect_interface_constant_reflection_members(
Vec::new(),
Vec::new(),
value,
interface_info
.constant_types
.get(constant_name)
.and_then(reflection_declared_type_metadata),
Visibility::Public,
is_final,
false,
Expand Down Expand Up @@ -2809,6 +2842,11 @@ fn reflection_trait_constant_reflection_members(
Vec::new(),
Vec::new(),
value,
ctx.module
.declared_trait_constant_types
.get(trait_name)
.and_then(|types| types.get(constant_name))
.and_then(reflection_declared_type_metadata),
ctx.module
.declared_trait_constant_visibilities
.get(trait_name)
Expand All @@ -2831,6 +2869,7 @@ fn push_unique_constant_reflection_member(
attr_names: Vec<String>,
attr_args: Vec<Option<Vec<AttrArgEntry>>>,
value: ReflectionConstantValue,
type_metadata: Option<ReflectionParameterTypeMetadata>,
visibility: Visibility,
is_final: bool,
is_enum_case: bool,
Expand All @@ -2850,7 +2889,7 @@ fn push_unique_constant_reflection_member(
is_enum_case,
flags: reflection_member_flags(false, &visibility, is_final, false, false, false),
modifiers: reflection_class_constant_modifiers(&visibility, is_final),
type_metadata: None,
type_metadata,
default_value: None,
property_hook_members: Vec::new(),
required_parameter_count: 0,
Expand Down Expand Up @@ -4306,6 +4345,39 @@ fn reflection_parameter_type_metadata(
}
}

/// Converts a retained declared class-constant type into reflection metadata.
fn reflection_declared_type_metadata(
type_expr: &TypeExpr,
) -> Option<ReflectionParameterTypeMetadata> {
match type_expr {
TypeExpr::Nullable(inner) => {
let mut metadata = reflection_named_type_metadata_from_type_expr(inner)?;
metadata.allows_null = true;
Some(ReflectionParameterTypeMetadata::Named(metadata))
}
TypeExpr::Union(members) => {
let allows_null = members.iter().any(|member| matches!(member, TypeExpr::Void));
let types = members
.iter()
.filter(|member| !matches!(member, TypeExpr::Void))
.map(reflection_named_type_metadata_from_type_expr)
.collect::<Option<Vec<_>>>()?;
if types.len() == 1 {
let mut metadata = types.into_iter().next()?;
metadata.allows_null = allows_null;
Some(ReflectionParameterTypeMetadata::Named(metadata))
} else {
(!types.is_empty()).then_some(ReflectionParameterTypeMetadata::Union(
ReflectionUnionTypeMetadata { types, allows_null },
))
}
}
TypeExpr::Intersection(members) => reflection_intersection_type_metadata(members),
_ => reflection_named_type_metadata_from_type_expr(type_expr)
.map(ReflectionParameterTypeMetadata::Named),
}
}

/// Converts a declared return type into the supported `ReflectionType` subset.
fn reflection_return_type_metadata(sig: &FunctionSig) -> Option<ReflectionParameterTypeMetadata> {
if !sig.declared_return {
Expand Down Expand Up @@ -4398,15 +4470,17 @@ fn reflection_named_type_metadata_from_type_expr(
TypeExpr::Int => Some(reflection_builtin_named_type("int", false)),
TypeExpr::Float => Some(reflection_builtin_named_type("float", false)),
TypeExpr::Bool => Some(reflection_builtin_named_type("bool", false)),
TypeExpr::False => Some(reflection_builtin_named_type("false", false)),
TypeExpr::Str => Some(reflection_builtin_named_type("string", false)),
TypeExpr::Iterable => Some(reflection_builtin_named_type("iterable", false)),
TypeExpr::Array(_) => Some(reflection_builtin_named_type("array", false)),
TypeExpr::Named(name) => {
let raw_name = name.as_str().trim_start_matches('\\');
match raw_name.to_ascii_lowercase().as_str() {
"array" | "callable" | "mixed" | "object" => {
"array" | "callable" | "object" => {
Some(reflection_builtin_named_type(raw_name, false))
}
"mixed" => Some(reflection_builtin_named_type(raw_name, true)),
_ => Some(ReflectionNamedTypeMetadata {
name: raw_name.to_string(),
allows_null: false,
Expand Down Expand Up @@ -6371,6 +6445,15 @@ fn emit_reflection_member_object(
&property_string,
)?;
}
if member_class_name == "ReflectionClassConstant" {
emit_reflection_owner_bool_property(
ctx,
member_class_name,
"__has_type",
member.type_metadata.is_some(),
)?;
emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?;
}
if matches!(
member_class_name,
"ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase"
Expand Down
1 change: 1 addition & 0 deletions src/codegen_support/runtime/data/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,7 @@ mod tests {
is_readonly_class: false,
allow_dynamic_properties: false,
constants: HashMap::new(),
constant_types: HashMap::new(),
constant_visibilities: HashMap::new(),
final_constants: HashSet::new(),
attribute_names: Vec::new(),
Expand Down
3 changes: 3 additions & 0 deletions src/ir/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub struct Module {
pub declared_trait_property_names: HashMap<String, Vec<String>>,
pub declared_trait_constant_names: HashMap<String, Vec<String>>,
pub declared_trait_constants: HashMap<String, HashMap<String, crate::parser::ast::Expr>>,
pub declared_trait_constant_types:
HashMap<String, HashMap<String, crate::parser::ast::TypeExpr>>,
pub declared_trait_constant_visibilities: HashMap<String, HashMap<String, Visibility>>,
pub declared_trait_final_constants: HashMap<String, HashSet<String>>,
/// Prescanned global constant values used by EIR lowering and eval metadata registration.
Expand Down Expand Up @@ -119,6 +121,7 @@ impl Module {
declared_trait_property_names: HashMap::new(),
declared_trait_constant_names: HashMap::new(),
declared_trait_constants: HashMap::new(),
declared_trait_constant_types: HashMap::new(),
declared_trait_constant_visibilities: HashMap::new(),
declared_trait_final_constants: HashMap::new(),
global_constants: HashMap::new(),
Expand Down
35 changes: 35 additions & 0 deletions src/ir_lower/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec
module.declared_trait_property_names = collect_declared_trait_property_names(program);
module.declared_trait_constant_names = collect_declared_trait_constant_names(program);
module.declared_trait_constants = collect_declared_trait_constants(program);
module.declared_trait_constant_types = collect_declared_trait_constant_types(program);
module.declared_trait_constant_visibilities =
collect_declared_trait_constant_visibilities(program);
module.declared_trait_final_constants = collect_declared_trait_final_constants(program);
Expand Down Expand Up @@ -1367,6 +1368,40 @@ fn collect_declared_trait_constants(program: &Program) -> HashMap<String, HashMa
constants
}

/// Collects direct PHP declared constant types for each trait.
fn collect_declared_trait_constant_types(
program: &Program,
) -> HashMap<String, HashMap<String, crate::parser::ast::TypeExpr>> {
let mut types = HashMap::new();
for stmt in program {
match &stmt.kind {
StmtKind::TraitDecl {
name,
constants: trait_constants,
..
} => {
types.insert(
name.clone(),
trait_constants
.iter()
.filter_map(|constant| {
constant
.type_expr
.clone()
.map(|type_expr| (constant.name.clone(), type_expr))
})
.collect(),
);
}
StmtKind::NamespaceBlock { body, .. } => {
types.extend(collect_declared_trait_constant_types(body));
}
_ => {}
}
}
types
}

/// Collects direct PHP constant visibilities declared by each trait.
fn collect_declared_trait_constant_visibilities(
program: &Program,
Expand Down
2 changes: 2 additions & 0 deletions src/ir_lower/tests/exhaustive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ fn class_info(_class_name: &str) -> ClassInfo {
is_readonly_class: false,
allow_dynamic_properties: true,
constants: HashMap::new(),
constant_types: HashMap::new(),
constant_visibilities: Default::default(),
final_constants: Default::default(),
attribute_names: Vec::new(),
Expand Down Expand Up @@ -381,6 +382,7 @@ fn lowers_every_stmt_variant_smoke() {
name: "K".to_string(),
visibility: Visibility::Public,
is_final: false,
type_expr: None,
value: int(1),
span: sp(),
attributes: Vec::new(),
Expand Down
4 changes: 4 additions & 0 deletions src/name_resolver/declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ fn resolve_class_consts(
constants
.iter()
.map(|constant| ClassConst {
type_expr: constant
.type_expr
.as_ref()
.map(|ty| resolve_type_expr(ty, namespace, imports, symbols)),
value: resolve_expr(&constant.value, namespace, imports, symbols),
attributes: resolve_attribute_groups(&constant.attributes, namespace, imports, symbols),
..constant.clone()
Expand Down
14 changes: 8 additions & 6 deletions src/parser/ast/oop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,16 @@ impl PartialEq for ClassProperty {
}
}

/// `const NAME = expr;` declaration inside a class/interface/trait body.
/// PHP supports per-constant visibility (PHP 7.1+) and the `final`
/// modifier (PHP 8.1+). Per-constant attributes are stored for future
/// `#[\Deprecated]` support.
/// `const [TYPE] NAME = expr;` declaration inside a class/interface/trait body.
/// PHP supports per-constant visibility (PHP 7.1+), the `final` modifier
/// (PHP 8.1+), and declared types (PHP 8.3+). Per-constant attributes are
/// retained for reflection.
#[derive(Debug, Clone)]
pub struct ClassConst {
pub name: String,
pub visibility: Visibility,
pub is_final: bool,
pub type_expr: Option<TypeExpr>,
pub value: Expr,
#[allow(dead_code)] // Used for error reporting in future passes
pub span: Span,
Expand All @@ -192,12 +193,13 @@ pub struct ClassConst {
}

impl PartialEq for ClassConst {
/// Compares class constants by name, visibility, is_final, value, and attributes;
/// span is not compared.
/// Compares class constants by name, visibility, final/type metadata, value, and
/// attributes; span is not compared.
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.visibility == other.visibility
&& self.is_final == other.is_final
&& self.type_expr == other.type_expr
&& self.value == other.value
&& self.attributes == other.attributes
}
Expand Down
Loading
Loading