Skip to content
Open
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 src/codegen/lower_inst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId)
Op::BufferGet => buffers::lower_buffer_get(ctx, &inst),
Op::BufferSet => buffers::lower_buffer_set(ctx, &inst),
Op::ObjectNew => objects::lower_object_new(ctx, &inst),
Op::ObjectCloneShallow => objects::lower_object_clone_shallow(ctx, &inst),
Op::DynamicObjectNew => objects::lower_dynamic_object_new(ctx, &inst),
Op::DynamicObjectNewMixed => objects::lower_dynamic_object_new_mixed(ctx, &inst),
Op::PropGet => objects::lower_prop_get(ctx, &inst),
Expand Down
62 changes: 62 additions & 0 deletions src/codegen/lower_inst/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,68 @@ pub(super) fn lower_object_new(ctx: &mut FunctionContext<'_>, inst: &Instruction
Ok(())
}

/// Lowers `object_clone_shallow` (PHP `clone`): allocates a same-class payload and copies the
/// class-id word plus every property slot pair from the source instance, unrolled at the
/// statically-known payload size. Reference-property slots copy their CELL POINTER, which is
/// PHP semantics (a reference property stays a reference shared with the original). Classes
/// with dynamic properties are rejected — a plain payload copy would share the
/// dynamic-property hash, which PHP does not do.
pub(super) fn lower_object_clone_shallow(
ctx: &mut FunctionContext<'_>,
inst: &Instruction,
) -> Result<()> {
let class_name = class_name_immediate(ctx, inst)?.to_string();
let payload_size = {
let class_info = ctx
.module
.class_infos
.get(&class_name)
.ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?;
if class_info.allow_dynamic_properties {
return Err(CodegenIrError::unsupported(format!(
"clone of dynamic-properties class {}",
class_name
)));
}
dynamic_property_hash_offset(class_info.properties.len())
};
let source = expect_operand(inst, 0)?;
ctx.load_value_to_result(source)?;
abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter));
match ctx.emitter.target.arch {
Arch::AArch64 => {
abi::emit_load_int_immediate(ctx.emitter, "x0", payload_size as i64); // request clone payload storage matching the source class layout
abi::emit_call_label(ctx.emitter, "__rt_heap_alloc");
ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers
ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the clone payload
abi::emit_pop_reg(ctx.emitter, "x11"); // reload the source object pointer
}
Arch::X86_64 => {
abi::emit_load_int_immediate(ctx.emitter, "rax", payload_size as i64); // request clone payload storage matching the source class layout
abi::emit_call_label(ctx.emitter, "__rt_heap_alloc");
ctx.emitter.instruction(&format!(
"mov r10, 0x{:x}",
(X86_64_HEAP_MAGIC_HI32 << 32) | 4
)); // materialize the x86_64 object heap kind word
ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the clone payload
abi::emit_pop_reg(ctx.emitter, "r11"); // reload the source object pointer
}
}
let dst_reg = abi::int_result_reg(ctx.emitter);
let scratch = abi::secondary_scratch_reg(ctx.emitter);
let src_reg = match ctx.emitter.target.arch {
Arch::AArch64 => "x11",
Arch::X86_64 => "r11",
};
let mut offset = 0usize;
while offset < payload_size {
abi::emit_load_from_address(ctx.emitter, scratch, src_reg, offset);
abi::emit_store_to_address(ctx.emitter, scratch, dst_reg, offset);
offset += 8;
}
store_if_result(ctx, inst)
}

/// Lowers `new stdClass()` through the runtime helper that seeds its dynamic-property hash.
fn lower_stdclass_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> {
if !inst.operands.is_empty() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet<String>
| ExprKind::ErrorSuppress(expr)
| ExprKind::Print(expr)
| ExprKind::Spread(expr)
| ExprKind::Clone(expr)
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. } => collect_required_class_names_in_expr(expr, names),
ExprKind::NullCoalesce { value, default } => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(expr)
| ExprKind::Print(expr)
| ExprKind::Spread(expr)
| ExprKind::Clone(expr)
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. }
| ExprKind::NamedArg { value: expr, .. }
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 @@ -340,6 +340,7 @@ fn expr_has_regex_call(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(expr)
| ExprKind::Print(expr)
| ExprKind::Spread(expr)
| ExprKind::Clone(expr)
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. }
| ExprKind::YieldFrom(expr) => expr_has_regex_call(expr),
Expand Down Expand Up @@ -651,6 +652,7 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(expr)
| ExprKind::Print(expr)
| ExprKind::Spread(expr)
| ExprKind::Clone(expr)
| ExprKind::Cast { expr, .. }
| ExprKind::PtrCast { expr, .. }
| ExprKind::YieldFrom(expr) => expr_needs_descriptor_invoker(expr),
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 @@ -251,6 +251,7 @@ fn expr_refs_image(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner)
| ExprKind::Clone(inner)
| ExprKind::YieldFrom(inner) => expr_refs_image(inner),
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default } => {
Expand Down
7 changes: 6 additions & 1 deletion src/ir/instr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ pub enum Op {
IteratorMethodCall,
SplRuntimeCall,
ObjectNew,
/// Shallow-copies an object instance (PHP `clone`): allocates a same-class payload and
/// copies the class-id word and every property slot pair. Operand: source object;
/// immediate: class-name data id (the operand's statically-checked class).
ObjectCloneShallow,
DynamicObjectNew,
DynamicObjectNewMixed,
PropGet,
Expand Down Expand Up @@ -459,7 +463,7 @@ impl Op {
}
ArrayGet => E::READS_HEAP | E::MAY_FATAL | E::MAY_WARN,
StrPersist | ArrayEnsureUnique | HashEnsureUnique | ArrayCloneShallow
| HashCloneShallow => E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP,
| HashCloneShallow | ObjectCloneShallow => E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP,
ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | LoadPropRefCell => {
E::READS_HEAP
}
Expand Down Expand Up @@ -664,6 +668,7 @@ impl Op {
IteratorMethodCall => "iterator_method_call",
SplRuntimeCall => "spl_runtime_call",
ObjectNew => "object_new",
ObjectCloneShallow => "object_clone_shallow",
DynamicObjectNew => "dynamic_object_new",
DynamicObjectNewMixed => "dynamic_object_new_mixed",
PropGet => "prop_get",
Expand Down
24 changes: 24 additions & 0 deletions src/ir_lower/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe
ExprKind::ExprCall { callee, args } => lower_expr_call(ctx, callee, args, expr),
ExprKind::ConstRef(name) => constants::lower_const_ref(ctx, name, expr),
ExprKind::NewObject { class_name, args } => lower_new_object(ctx, class_name, args, expr),
ExprKind::Clone(inner) => lower_clone(ctx, inner, expr),
ExprKind::NewDynamic { name_expr, args } => {
lower_new_dynamic(ctx, name_expr, args, expr)
}
Expand Down Expand Up @@ -681,6 +682,7 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool {
| ExprKind::NullsafeMethodCall { .. }
| ExprKind::StaticMethodCall { .. }
| ExprKind::NewObject { .. }
| ExprKind::Clone(..)
| ExprKind::NewDynamic { .. }
| ExprKind::NewDynamicObject { .. }
| ExprKind::NewScopedObject { .. }
Expand Down Expand Up @@ -8323,6 +8325,28 @@ fn lower_new_object(ctx: &mut LoweringContext<'_, '_>, class_name: &Name, args:
)
}

/// Lowers PHP `clone $expr` into the shallow object-copy EIR opcode. The checker guarantees
/// the operand is a statically-typed object, so the class name rides along as the immediate
/// and the backend derives the payload layout from the class metadata. A non-object operand
/// (unreachable past the checker) passes the source value through unchanged.
fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> LoweredValue {
let source = lower_expr(ctx, inner);
let source_ty = ctx.builder.value_php_type(source.value);
let PhpType::Object(class_name) = source_ty.clone() else {
debug_assert!(false, "checker admitted clone of {:?}", source_ty);
return source;
};
let data = ctx.intern_class_name(&class_name);
ctx.emit_value(
Op::ObjectCloneShallow,
vec![source.value],
Some(Immediate::Data(data)),
PhpType::Object(class_name),
Op::ObjectCloneShallow.default_effects(),
Some(expr.span),
)
}

/// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode.
fn lower_new_dynamic(
ctx: &mut LoweringContext<'_, '_>,
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 @@ -158,6 +158,7 @@ fn expr_refs_listid(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner)
| ExprKind::Clone(inner)
| ExprKind::YieldFrom(inner) => expr_refs_listid(inner),
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default } => {
Expand Down
1 change: 1 addition & 0 deletions src/magic_constants/walker/exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ pub(super) fn walk_expr<P: Pass>(expr: Expr, pass: &mut P) -> Expr {
required,
},
ExprKind::Spread(inner) => ExprKind::Spread(Box::new(walk_expr(*inner, pass))),
ExprKind::Clone(inner) => ExprKind::Clone(Box::new(walk_expr(*inner, pass))),
ExprKind::ClosureCall { var, args } => ExprKind::ClosureCall {
var,
args: args.into_iter().map(|a| walk_expr(a, pass)).collect(),
Expand Down
6 changes: 6 additions & 0 deletions src/name_resolver/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,12 @@ pub(super) fn resolve_expr(
name: name.clone(),
value: Box::new(resolve_expr(value, current_namespace, imports, symbols)),
},
ExprKind::Clone(inner) => ExprKind::Clone(Box::new(resolve_expr(
inner,
current_namespace,
imports,
symbols,
))),
_ => expr.kind.clone(),
};
Expr::new(kind, expr.span)
Expand Down
1 change: 1 addition & 0 deletions src/optimize/control/prune/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr {
value: Box::new(prune_expr(*value)),
},
ExprKind::Spread(inner) => ExprKind::Spread(Box::new(prune_expr(*inner))),
ExprKind::Clone(inner) => ExprKind::Clone(Box::new(prune_expr(*inner))),
ExprKind::ClosureCall { var, args } => ExprKind::ClosureCall {
var,
args: args.into_iter().map(prune_expr).collect(),
Expand Down
1 change: 1 addition & 0 deletions src/optimize/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect {
ExprKind::ExprCall { callee, args } => expr_effect(callee)
.combine(combine_effects(args.iter().map(expr_effect)))
.combine(expr_call_effect(callee)),
ExprKind::Clone(inner) => expr_effect(inner).with_side_effects().with_may_throw(),
ExprKind::NewObject { args, .. } => combine_effects(args.iter().map(expr_effect))
.with_side_effects()
.with_may_throw(),
Expand Down
1 change: 1 addition & 0 deletions src/optimize/fold/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr {
value: Box::new(fold_expr(*value)),
},
ExprKind::Spread(inner) => ExprKind::Spread(Box::new(fold_expr(*inner))),
ExprKind::Clone(inner) => ExprKind::Clone(Box::new(fold_expr(*inner))),
ExprKind::ClosureCall { var, args } => ExprKind::ClosureCall {
var,
args: args.into_iter().map(fold_expr).collect(),
Expand Down
1 change: 1 addition & 0 deletions src/optimize/propagate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr {
value: Box::new(propagate_expr(*value, env)),
},
ExprKind::Spread(inner) => ExprKind::Spread(Box::new(propagate_expr(*inner, env))),
ExprKind::Clone(inner) => ExprKind::Clone(Box::new(propagate_expr(*inner, env))),
ExprKind::ClosureCall { var, args } => {
let arg_env = (!callable_alias_effect(&var).has_side_effects).then_some(env);
ExprKind::ClosureCall {
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 @@ -112,6 +112,7 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation {
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner)
| ExprKind::Clone(inner)
| ExprKind::PtrCast { expr: inner, .. }
| ExprKind::Cast { expr: inner, .. }
| ExprKind::BufferNew { len: inner, .. }
Expand Down
2 changes: 2 additions & 0 deletions src/parser/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub enum ExprKind {
name: String,
value: Box<Expr>,
},
/// PHP's `clone $expr` unary operator — a shallow copy of an object instance.
Clone(Box<Expr>),
/// A `require`/`include` used in expression position (e.g. `$x = require 'f.php';`).
///
/// This is a transient node produced by the parser and fully expanded by the resolver into
Expand Down
7 changes: 5 additions & 2 deletions src/parser/expr/assignment_targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ 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::Clone(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 @@ -470,7 +471,8 @@ 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::Clone(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 @@ -669,6 +671,7 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool {
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::NamedArg { value, .. }
| ExprKind::Spread(value)
| ExprKind::Clone(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
29 changes: 29 additions & 0 deletions src/parser/expr/prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ pub(super) fn parse_prefix(
Token::Function => parse_closure(tokens, pos, span, false),
Token::Fn => parse_arrow_closure(tokens, pos, span, false),
Token::AttrOpen => parse_attributed_closure(tokens, pos, span),
// `clone <expr>` — PHP's clone operator. `clone` is not a reserved token (it stays
// usable as a method/member name), so it is recognized contextually: an identifier
// spelled `clone` in prefix position followed by a token that can start its operand.
// `clone($x)` (parenthesized operand) lands here too, matching PHP.
Token::Identifier(name)
if name.eq_ignore_ascii_case("clone") && clone_operand_follows(tokens, *pos + 1) =>
{
parse_unary(tokens, pos, span, ExprKind::Clone, 35)
}
Token::Identifier(_) | Token::Backslash => parse_named_expr(tokens, pos, span),
Token::Self_ => {
*pos += 1;
Expand Down Expand Up @@ -337,6 +346,26 @@ fn parse_unary(
Ok(Expr::new(ctor(Box::new(inner)), span))
}

/// Returns true when the token at `pos` can begin a `clone` operand. This keeps the contextual
/// `clone` recognition from swallowing an identifier merely SPELLED "clone" in non-operator
/// positions (`clone;`, `clone,`, `clone)` …), which keeps parsing as a plain name.
fn clone_operand_follows(tokens: &[(Token, Span)], pos: usize) -> bool {
match tokens.get(pos).map(|(token, _)| token) {
Some(
Token::Variable(_)
| Token::This
| Token::Identifier(_)
| Token::Backslash
| Token::New
| Token::Self_
| Token::Static
| Token::Parent
| Token::LParen,
) => true,
_ => false,
}
}

/// Parses a prefix `++` or `--` increment/decrement operator. Consumes the operator,
/// then expects a `Variable` token next. Returns `PreIncrement` or `PreDecrement` with the
/// variable name. Returns an error if a variable does not follow the operator.
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 @@ -344,6 +344,7 @@ fn collect_arrow_expr_captures(
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner)
| ExprKind::Clone(inner)
| ExprKind::PtrCast { expr: inner, .. }
| ExprKind::Cast { expr: inner, .. }
| ExprKind::YieldFrom(inner) => collect_arrow_expr_captures(inner, 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 @@ -188,6 +188,7 @@ fn expr_refs_pdo(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner)
| ExprKind::Clone(inner)
| ExprKind::YieldFrom(inner) => expr_refs_pdo(inner),
ExprKind::NullCoalesce { value, default }
| ExprKind::ShortTernary { value, default } => {
Expand Down
1 change: 1 addition & 0 deletions src/resolver/contains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ fn expr_has_includes(expr: &Expr) -> bool {
| ExprKind::ErrorSuppress(value)
| ExprKind::Print(value)
| ExprKind::Spread(value)
| ExprKind::Clone(value)
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::BufferNew { len: value, .. } => expr_has_includes(value),
ExprKind::NullCoalesce { value, default }
Expand Down
1 change: 1 addition & 0 deletions src/resolver/discovery/exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub(super) fn discover_expr(
| ExprKind::ErrorSuppress(value)
| ExprKind::Print(value)
| ExprKind::Spread(value)
| ExprKind::Clone(value)
| ExprKind::PtrCast { expr: value, .. }
| ExprKind::BufferNew { len: value, .. } => {
discover_expr(value, base_dir, loaded_paths, include_chain, state, output)?;
Expand Down
3 changes: 2 additions & 1 deletion src/types/checker/inference/expr/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ impl Checker {
| ExprKind::Throw(inner)
| ExprKind::ErrorSuppress(inner)
| ExprKind::Print(inner)
| ExprKind::Spread(inner) => {
| ExprKind::Spread(inner)
| ExprKind::Clone(inner) => {
self.infer_type_with_assignment_effects(inner, env)?;
self.infer_type(expr, env)
}
Expand Down
Loading
Loading