-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Clone with #9497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clone with #9497
Conversation
5e388c6
to
4db8ae5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable overall. I'm a bit unsure if unset
should be allowed because that also means the property can be uninitialized after clone
but I guess that would be consistent with new
.
f143dde
to
82a6cb1
Compare
82a6cb1
to
7f7d7f5
Compare
My latest commit adds support for "clone with". It seems to be working for me, but I'd appreciate any review and testing. cc @nicolas-grekas |
a65ac9b
to
3189938
Compare
3189938
to
8075bde
Compare
Thanks for working on this! I'm wondering: how is __clone() going to interact with clone-with? Will __clone() be called before clone-with properties are set? I guess it would make sense this way. Can you add a test case for this behavior please? I've already told so on other channels: I personally think that the proposed "clone-with" syntax should be split from the patch that unlocks reinitializing readonly properties during cloning. The reason is that the new syntax might raise a lengthy discussion, while reinitializing readonly properties during cloning should be consensual since it's fixing a limitation that has not reasons to exist. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test implementations should be run twice in the PHP --TEST--
body itself (expecting the same output twice) to properly test the optimized handlers - the unoptimized implementation of ZEND_INIT_OBJ is what's being tested right now, not the optimizations, which seem to have bugs
Also, there could be more tests where clone with
throws
- TypeError from incompatible properties (twice)
- An exception from clone with overwriting a declared untyped property that has
__destruct
- i.e.clone function_return_obj_with_myProperty_val_throwing_in_destructor() with {myProperty: new stdClass()}
- Same, but for
#[AllowDynamicProperties]
and a property with no declaration
Zend/zend_vm_def.h
Outdated
if (UNEXPECTED(RETURN_VALUE_USED(opline))) { | ||
ZVAL_COPY(EX_VAR(opline->result.var), value); | ||
} | ||
ZEND_VM_C_GOTO(exit_init_obj); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be ZEND_VM_C_GOTO(free_and_exit_init_obj)
instead, like the above branch?
Can the label for exit_init_obj
be deleted entirely? I'd hope so.
If this opcode were to throw, then php relies on the temporary value being placed in the return value's temporary variable to free it.
I think this isn't reached in any of the unit tests,
and the error handler checking for ZEND_INIT_OBJ is why this passes?
For many of the tests, they should be run in a PHP for loop to run twice, to check that they do the same thing twice in a row with the same run time cache.
if (UNEXPECTED((Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE)) ||
(UNEXPECTED(EG(current_execute_data) && EG(current_execute_data)->opline &&
EG(current_execute_data)->opline->opcode == ZEND_INIT_OBJ))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same question arises: does OP_DATA
have to be freed or not? And why ZEND_ASSIGN_OBJ
doesn't do it?
ZEND_VM_C_LABEL(init_object): | ||
zobj = Z_OBJ_P(object); | ||
void **cache_slot = CACHE_ADDR(opline->extended_value); | ||
if (EXPECTED(zobj->ce == CACHED_PTR(opline->extended_value))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (EXPECTED(zobj->ce == CACHED_PTR(opline->extended_value))) {
- This code path will only be reached the second time you execute code.
So for many of the tests, they should be run in a for loop to run twice, to check that they do the same thing twice in a row.
The --repeat
option won't help, since I'm pretty sure the run time cache is distinct for separate php requests
It still makes sense to have, since the optimized code will be faster in applications that call a given php clone with
expression multiple times
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this fast path really needed? That might just might increase cache pressure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yes, good question! I'm not yet sure... I guess if the opcode is not used often then the gain with the cache is not much. In the same time, if the cache is not used much then it doesn't cause a big pressure (?). Anyway, I'm open to remove this path (given the RFC passes) :)
e40da2f
to
f36a12e
Compare
f36a12e
to
a5dd175
Compare
I think there needs to be a test to ensure that |
Makes sense, thanks for the idea! So far only the private scope of non-readonly properties has been tested. |
a5dd175
to
75355cf
Compare
75355cf
to
b9a9baa
Compare
Why can't we simple reuse constructor? function withB($b) {
return new self(a: $this->a, b: $b, c: $this->c);
} |
I might be not popular with this, but did you consider using an array syntax for |
Wasn't the idea to pass the arguments to |
@iluuu1994 Passing arguments to @dstogov That mandates that the constructor be a 1:1 map of the properties of the object. That's only viable in a small fraction of use cases. It also means that every time you increase the number of properties, you also add another line to every |
Thank you @Crell for your reply! I completely agree with these arguments. :) |
|
||
if (UNEXPECTED(is_readonly)) { | ||
is_clone_with_op = EG(current_execute_data) && EG(current_execute_data)->opline && | ||
EG(current_execute_data)->opline->opcode == ZEND_CLONE_INIT_PROP; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this re-use the IS_PROP_REINITABLE
flag? Otherwise, any path that currently checks for IS_PROP_REINITABLE
needs to be adjusted (see JIT).
ZEND_VM_C_LABEL(init_object): | ||
zobj = Z_OBJ_P(object); | ||
void **cache_slot = CACHE_ADDR(opline->extended_value); | ||
if (EXPECTED(zobj->ce == CACHED_PTR(opline->extended_value))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this fast path really needed? That might just might increase cache pressure.
/* Unlocks readonly properties during cloning for exactly one assignment. It has no impact on regular properties. */ | ||
#define IS_PROP_REINITABLE (1<<1) | ||
/* Re-locks readonly properties during "clone with" operations. It has no impact on regular properties. */ | ||
#define IS_PROP_REINITED (1<<2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between IS_PROP_REINITED
and the absence of the IS_PROP_REINITABLE
flag? Do we need distinction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, the distinction is very subtle, but I didn't manage to come up with an easier to understand solution. Basically, the difference between them is the following:
IS_PROP_REINITABLE
indicates that a property is modifiable by any operation because__clone()
unlocked the possibility. Then - after__clone()
finishes, the possibility is gone and the flag is removed.IS_PROP_REINITED
indicates that a property is not modifiable anymore in theZEND_CLONE_INIT_PROP
opcode. Here, we cannot rely on the absense ofIS_PROP_REINITABLE
because we cannot make these propertiesIS_PROP_REINITABLE
in advance. Therefore we can only re-lock them when the assignment happened.
b9a9baa
to
4d184f9
Compare
4d184f9
to
2f31b13
Compare
Co-Authored-By: Arnaud Le Blanc <arnaud.lb@gmail.com>
fae77f4
to
2d394b2
Compare
2d394b2
to
c9f9913
Compare
Perhaps I'm dumb but wouldn't it be much easier to just redesign "clone $object" to be handled like "new class"? e.g.
cloning would then still be allowed anywhere without the need to check if it's inside a method of the class to be cloned the godawful ness as per below is a result of it's usage not the implementation of having arguments on ___clone
One could put complex the logic that would be required in both __construct and __clone in a separate function as already allowed in __construct() |
@rraat the thing is your example is simplified too much. In real world you will have multiple public readonly properties in a single class and you don't want to make Cloning is done in different contexts, sometimes you want to override property X, sometimes Y. With explicit signature you would need to pass all the values every time you want to clone an object. The approach with final readonly class Foo {
public int $x;
public string $y;
public array $z;
public function __construct(
public int $a,
public int $b,
public string $c,
public string $d,
public array $e,
public array $f
) {
$this->resolve();
}
// You can't use `= null` for each argument to make it optional
// since it's ambiguous (you want to assign `null` or skip the overriding?)
public function __clone(
public int $a,
public int $b,
public string $c,
public string $d,
public array $e,
public array $f
) {
$this->resolve();
}
private function resolve() {
$this->x = $this->a + $this->b;
$this->y = $this->c . $this->d;
$this->z = array_merge($this->e, $this->f);
}
}
$foo = new Foo(1, 2, '3', '4', [5], [6]);
// Superfluous and cumbersome
$bar = clone $foo($foo->a, $foo->b, $foo->c, $foo->d, $foo->e, [7]);
// Short and clear
$baz = clone $foo with ['f' => [7]]; As you can see, even with extracting the logic to a method shared between |
"clone ... With []" vs "clone ... ()" doesn't really make much difference it's about the scope where and when things are allowed/unlocked, the point is more about having the changed fields send to __clone or not. when not sending the fields to update to __clone
This creates a situation where "clone" and even "clone with" has different behaviour depending on where it is used and on what members. when fields to update ARE send to __clone
__clone would be the only place that readonly properties would need to be unlocked (just like in __construct) and would only be allow to change properties that would normally be allowed to be changed in __construct, to update properties on the parent one could call parent::__clone (just like parent::__construct) How __clone is defined in a class would make the detection of provided or default possible or not It seems just so much simpler to implement, explain and use, to have __clone handle cloning then doing it in both __clone and outside of it (all over the place). Finally You can make __clone as complicated/simple as you want.
but then again... that might just be me. p.s. "readonly clone ... with" is effectively the property equivalent of having interface with "get/with" methods (interfaces with with/get shouldn't need to use readonly properties as we are using the interface to access them) |
Isn't something like this added in version 8.4 ? :( |
Isn't this feature going to be added ? 😒 |
No, I decided not to pursue this. Properties are already extremely difficult handling after some recent changes, and I don't want to complicate the situation even more - at least this is what I think now. Of course anyway is free to pick up the RFC and the implementation (which became quite much outdated in the meanwhile). |
This PR used to provide implementation for https://wiki.php.net/rfc/readonly_amendments, but those changes were split off to #10389.
Now, this PR only implements the https://wiki.php.net/rfc/clone_with RFC.