diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index ba362e3f93..1fc84c9dbc 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -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), diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 1fc999ca5c..71480d3333 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -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() { diff --git a/src/codegen_support/program_usage/required_classes/collect.rs b/src/codegen_support/program_usage/required_classes/collect.rs index f42f6317bd..90320d1778 100644 --- a/src/codegen_support/program_usage/required_classes/collect.rs +++ b/src/codegen_support/program_usage/required_classes/collect.rs @@ -288,6 +288,7 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet | 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 } => { diff --git a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs index 1a5498843b..2b4eddc0c6 100644 --- a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs +++ b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs @@ -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, .. } diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 63ab6ad9c5..80eb97bb27 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -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), @@ -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), diff --git a/src/image_prelude/detect.rs b/src/image_prelude/detect.rs index a3dd2163a8..f5b903d57d 100644 --- a/src/image_prelude/detect.rs +++ b/src/image_prelude/detect.rs @@ -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 } => { diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 389ff8e84a..b36815fa72 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -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, @@ -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 } @@ -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", diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 71a2f70ede..6fbb0d6acf 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -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) } @@ -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 { .. } @@ -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<'_, '_>, diff --git a/src/list_id_prelude/detect.rs b/src/list_id_prelude/detect.rs index 20175e71da..e203ad55ca 100644 --- a/src/list_id_prelude/detect.rs +++ b/src/list_id_prelude/detect.rs @@ -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 } => { diff --git a/src/magic_constants/walker/exprs.rs b/src/magic_constants/walker/exprs.rs index 57b3596849..f4769553c9 100644 --- a/src/magic_constants/walker/exprs.rs +++ b/src/magic_constants/walker/exprs.rs @@ -174,6 +174,7 @@ pub(super) fn walk_expr(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(), diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index 98729e303f..513e107258 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -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) diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index 6bb9af9289..668494812a 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -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(), diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index 9fc6574987..b82ce32342 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -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(), diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index 99b17399c6..17143f5579 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -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(), diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index fd3c1e287b..79cae9ab7a 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -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 { diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index 7d3732f866..f0bb75fc70 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -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, .. } diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index d8237a80aa..c00cd4ba5c 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -115,6 +115,8 @@ pub enum ExprKind { name: String, value: Box, }, + /// PHP's `clone $expr` unary operator — a shallow copy of an object instance. + Clone(Box), /// 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 diff --git a/src/parser/expr/assignment_targets.rs b/src/parser/expr/assignment_targets.rs index b067301187..317af3af7e 100644 --- a/src/parser/expr/assignment_targets.rs +++ b/src/parser/expr/assignment_targets.rs @@ -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); @@ -470,7 +471,8 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> 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) @@ -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) diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index 311d764012..d50e0400b8 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -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 ` — 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; @@ -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. diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index d79c3db462..ba4ee095fc 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -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), diff --git a/src/pdo_prelude/detect.rs b/src/pdo_prelude/detect.rs index d4a141043b..ad21a268b3 100644 --- a/src/pdo_prelude/detect.rs +++ b/src/pdo_prelude/detect.rs @@ -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 } => { diff --git a/src/resolver/contains.rs b/src/resolver/contains.rs index 64888047aa..d7eaca8e69 100644 --- a/src/resolver/contains.rs +++ b/src/resolver/contains.rs @@ -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 } diff --git a/src/resolver/discovery/exprs.rs b/src/resolver/discovery/exprs.rs index 29f5895a8a..8fbd14eca5 100644 --- a/src/resolver/discovery/exprs.rs +++ b/src/resolver/discovery/exprs.rs @@ -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)?; diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 0d89efbbc0..3e0bdecae4 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -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) } diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index a8c6edbc44..2392ece48b 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -70,6 +70,35 @@ impl Checker { self.infer_type(inner, env)?; Ok(PhpType::Bool) } + // `clone $expr` — a shallow copy with the operand's own class. The class must be + // statically known (the backend computes the payload size from the class layout), + // and `__clone()` hooks are not modeled yet, so a class declaring one is rejected + // rather than silently skipping its hook. + ExprKind::Clone(inner) => { + let inner_ty = self.infer_type(inner, env)?; + let PhpType::Object(class_name) = &inner_ty else { + return Err(CompileError::new( + expr.span, + &format!("clone expects a statically-typed object, got {:?}", inner_ty), + )); + }; + if self + .classes + .get(class_name) + .is_some_and(|info| { + info.methods.contains_key(&crate::names::php_symbol_key("__clone")) + }) + { + return Err(CompileError::new( + expr.span, + &format!( + "clone of {}: __clone() hooks are not supported yet", + class_name + ), + )); + } + Ok(inner_ty) + } ExprKind::ErrorSuppress(inner) => self.infer_type(inner, env), ExprKind::Print(inner) => { self.infer_type(inner, env)?; diff --git a/src/types/checker/schema/classes/constants.rs b/src/types/checker/schema/classes/constants.rs index 9487774100..f546c27fec 100644 --- a/src/types/checker/schema/classes/constants.rs +++ b/src/types/checker/schema/classes/constants.rs @@ -199,6 +199,9 @@ fn rewrite_expr( ExprKind::Spread(inner) => { ExprKind::Spread(Box::new(rewrite_expr(inner, class_name, parent_name)?)) } + ExprKind::Clone(inner) => { + ExprKind::Clone(Box::new(rewrite_expr(inner, class_name, parent_name)?)) + } ExprKind::ClosureCall { var, args } => ExprKind::ClosureCall { var: var.clone(), args: rewrite_expr_list(args, class_name, parent_name)?, diff --git a/src/types/checker/yield_validation/validate.rs b/src/types/checker/yield_validation/validate.rs index d25a9f9e07..cd13726d55 100644 --- a/src/types/checker/yield_validation/validate.rs +++ b/src/types/checker/yield_validation/validate.rs @@ -286,6 +286,7 @@ fn visit_expr(expr: &Expr, st: &mut State) { | ExprKind::Throw(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Spread(inner) + | ExprKind::Clone(inner) | ExprKind::Cast { expr: inner, .. } | ExprKind::PtrCast { expr: inner, .. } => visit_expr(inner, st), ExprKind::NullCoalesce { value, default } => { diff --git a/src/types/warnings/expr_reads.rs b/src/types/warnings/expr_reads.rs index d6def10c74..53115bbd50 100644 --- a/src/types/warnings/expr_reads.rs +++ b/src/types/warnings/expr_reads.rs @@ -47,6 +47,7 @@ pub(super) fn collect_expr_reads( | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) + | ExprKind::Clone(inner) | ExprKind::PtrCast { expr: inner, .. } => collect_expr_reads(inner, scope, warnings), ExprKind::NullCoalesce { value, default } => { collect_expr_reads(value, scope, warnings); diff --git a/src/tz_prelude/detect.rs b/src/tz_prelude/detect.rs index 082b4aa99d..f137b129c4 100644 --- a/src/tz_prelude/detect.rs +++ b/src/tz_prelude/detect.rs @@ -159,6 +159,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) + | ExprKind::Clone(inner) | ExprKind::YieldFrom(inner) => expr_refs_tz(inner), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { diff --git a/src/var_export_prelude/detect.rs b/src/var_export_prelude/detect.rs index f61569e903..e168f05016 100644 --- a/src/var_export_prelude/detect.rs +++ b/src/var_export_prelude/detect.rs @@ -161,6 +161,7 @@ fn expr_refs_ve(expr: &Expr) -> bool { | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) + | ExprKind::Clone(inner) | ExprKind::YieldFrom(inner) => expr_refs_ve(inner), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index a3d303b00e..c392336644 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -33,6 +33,8 @@ mod intersection_types; mod dynamic_dispatch; #[path = "oop/misc.rs"] mod misc; +#[path = "oop/clone.rs"] +mod clone; #[path = "oop/attributes.rs"] mod attributes; #[path = "oop/constants.rs"] diff --git a/tests/codegen/oop/clone.rs b/tests/codegen/oop/clone.rs new file mode 100644 index 0000000000..e1a973cfcf --- /dev/null +++ b/tests/codegen/oop/clone.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Integration tests for PHP `clone` (EC-7 #490): shallow object copy — the PSR-7 withX() +//! immutability round-trip (mutating the clone leaves the original untouched, object-typed +//! property slots share their instance), plus `clone` staying usable as a method name and +//! the parenthesized `clone($x)` form. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Byte-parity verified against PHP 8.5 for every assertion string. + +use super::*; + +/// The ward-http PSR-7 pattern: `$new = clone $this; $new->prop = ...; return $new;`. +/// The original keeps its values (distinct payloads); the clone's object-typed slot shares +/// the assigned instance (shallow copy). +#[test] +fn test_clone_with_x_round_trip() { + let out = compile_and_run( + "proto = $p; return $new; } public function withInner(Inner $i): self { $new = clone $this; $new->inner = $i; return $new; } public function proto(): string { return $this->proto; } public function innerTag(): string { return $this->inner instanceof Inner ? $this->inner->tag : 'none'; } } function main(): void { $a = new Msg(); $b = $a->withProto('2.0'); $c = $b->withInner(new Inner('x')); echo $a->proto(), ':', $b->proto(), ':', $c->proto(), ':', $a->innerTag(), ':', $c->innerTag(); } main();", + ); + assert_eq!(out, "1.1:2.0:2.0:none:x"); +} + +/// `clone` stays contextual: a method literally named `clone()` still parses and dispatches, +/// `clone($x)` (parenthesized operand) clones, and a clone of a clone is a fresh copy. +#[test] +fn test_clone_keyword_stays_contextual() { + let out = compile_and_run( + "clone('repo'), ':', $q->v, ':', $q2->v; } main();", + ); + assert_eq!(out, "cloned-repo:7:7"); +}