-
Notifications
You must be signed in to change notification settings - Fork 7.8k
[RFC] Implement constants in traits #8888
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
Conversation
f52f0ab
to
0e3e89c
Compare
cea4096
to
15ffed3
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.
FWIW, I commented myself on the intent of each modification in the hope that it might help the review.
In this process, I noticed some unnecessary codes and some test cases that would be better to have, so I have also added a few commits to fix them.
@@ -164,7 +164,7 @@ void zend_optimizer_pass1(zend_op_array *op_array, zend_optimizer_ctx *ctx) | |||
if (ce) { | |||
zend_class_constant *cc = zend_hash_find_ptr( | |||
&ce->constants_table, Z_STR(ZEND_OP2_LITERAL(opline))); | |||
if (cc && (ZEND_CLASS_CONST_FLAGS(cc) & ZEND_ACC_PPP_MASK) == ZEND_ACC_PUBLIC) { | |||
if (cc && (ZEND_CLASS_CONST_FLAGS(cc) & ZEND_ACC_PPP_MASK) == ZEND_ACC_PUBLIC && !(ce->ce_flags & ZEND_ACC_TRAIT)) { |
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.
Commentary
This part is needed to prevent accessing trait constants directly when opcache is enabled.
The checking is usually done on the handler of ZEND_FETCH_CLASS_CONSTANT at runtime.
Zend/zend_compile.c
Outdated
if (c->ce->ce_flags & ZEND_ACC_TRAIT) { | ||
return 0; | ||
} else if (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_PUBLIC) { |
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.
Commentary
This condition is added to prevent accessing trait constants directly.
The current implementation checks direct accesses on runtime, the handler of ZEND_FETCH_CLASS_CONSTANT.
Trait constants are copied to the constants table of each composing class on zend_do_traits_constant_binding()
, and their ce is replaced to the class entry of the composing class on that copying. So this condition is only met on directly accessing trait constants.
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 you inline this comment dirrectly into the code?
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.
Done.
Zend/zend_constants.c
Outdated
if (UNEXPECTED(ce->ce_flags & ZEND_ACC_TRAIT)) { | ||
if ((flags & ZEND_FETCH_CLASS_SILENT) == 0) { | ||
zend_throw_error(NULL, "Cannot access trait constant %s::%s directly", ZSTR_VAL(class_name), ZSTR_VAL(constant_name)); | ||
} | ||
goto failure; | ||
} |
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.
Commentary
This part is added to prevent accessing trait constants directly on cases like \defined()
or \constant()
, etc.
On some cases trait constants are referenced outside of the handler of ZEND_FETCH_CLASS_CONSTANT.
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.
Inline this comment into the code please.
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.
Done.
static zend_always_inline bool check_trait_property_or_constant_value_compatibility(zend_class_entry *ce, zval *op1, zval *op2) /* {{{ */ | ||
{ | ||
bool is_compatible; | ||
zval op1_tmp, op2_tmp; | ||
|
||
/* if any of the values is a constant, we try to resolve it */ | ||
if (UNEXPECTED(Z_TYPE_P(op1) == IS_CONSTANT_AST)) { | ||
ZVAL_COPY_OR_DUP(&op1_tmp, op1); | ||
zval_update_constant_ex(&op1_tmp, ce); | ||
op1 = &op1_tmp; | ||
} | ||
if (UNEXPECTED(Z_TYPE_P(op2) == IS_CONSTANT_AST)) { | ||
ZVAL_COPY_OR_DUP(&op2_tmp, op2); | ||
zval_update_constant_ex(&op2_tmp, ce); | ||
op2 = &op2_tmp; | ||
} | ||
|
||
is_compatible = fast_is_identical_function(op1, op2); | ||
|
||
if (op1 == &op1_tmp) { | ||
zval_ptr_dtor_nogc(&op1_tmp); | ||
} | ||
if (op2 == &op2_tmp) { | ||
zval_ptr_dtor_nogc(&op2_tmp); | ||
} | ||
|
||
return is_compatible; | ||
} | ||
/* }}} */ |
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.
Commentary
This function is extracted out from the implementation of zend_do_traits_property_binding() to share the implementation of the value compatibility check between properties and constants.
Zend/zend_inheritance.c
Outdated
static zend_class_entry* find_first_constant_definition(zend_class_entry *ce, zend_class_entry **traits, size_t current_trait, zend_string *constant_name, zend_class_entry *colliding_ce) /* {{{ */ | ||
{ | ||
size_t i; | ||
|
||
if (colliding_ce == ce) { | ||
for (i = 0; i < current_trait; i++) { | ||
if (traits[i] | ||
&& zend_hash_exists(&traits[i]->constants_table, constant_name)) { | ||
return traits[i]; | ||
} | ||
} | ||
} | ||
|
||
return colliding_ce; | ||
} | ||
/* }}} */ |
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.
Commentary
This function is used to show the place of the existing conflicting definition in error messages when conflicts occur.
Since trait constants are flattened into the constants table of the composing class, and thus we lose information about which constant was defined in which trait, a process like this is needed to find the location of the first definition of the constant from traits.
The implementation is in line with the existing function for properties, which is now renamed to find_first_property_definition()
from find_first_definition()
.
Zend/zend_inheritance.c
Outdated
static bool do_trait_constant_check(zend_class_entry *ce, zend_class_constant *trait_constant, zend_string *name, zend_class_entry **traits, size_t current_trait) /* {{{ */ | ||
{ | ||
uint32_t flags_mask = ZEND_ACC_PPP_MASK | ZEND_ACC_FINAL; | ||
|
||
zval *zv = zend_hash_find_known_hash(&ce->constants_table, name); | ||
if (zv == NULL) { | ||
// No existing constant of the same name, so this one can be added | ||
return true; | ||
} | ||
|
||
zend_class_constant *existing_constant = Z_PTR_P(zv); | ||
|
||
if ((ZEND_CLASS_CONST_FLAGS(trait_constant) & flags_mask) == (ZEND_CLASS_CONST_FLAGS(existing_constant) & flags_mask)) { | ||
if (check_trait_property_or_constant_value_compatibility(ce, &trait_constant->value, &existing_constant->value)) { | ||
// There is an existing constant which is compatible with the new one, so no need to add it | ||
return false; | ||
} | ||
} | ||
|
||
// There is an existing constant of the same name, and it conflicts with the new one, so let's throw a fatal error | ||
zend_error_noreturn(E_COMPILE_ERROR, | ||
"%s and %s define the same constant (%s) in the composition of %s. However, the definition differs and is considered incompatible. Class was composed", | ||
ZSTR_VAL(find_first_constant_definition(ce, traits, current_trait, name, existing_constant->ce)->name), | ||
ZSTR_VAL(trait_constant->ce->name), | ||
ZSTR_VAL(name), | ||
ZSTR_VAL(ce->name)); | ||
} | ||
/* }}} */ |
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.
Commentary
This function checks for trait constants to be added to the composing class and has three possible outcomes:
- There is no existing constant of the same name, so it can be added.
- It is compatible with an existing constant of the same name, so it can be skipped without adding a new one.
- It conflicts with an existing constant of the same name, so triggers a fatal error.
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'm confused here, doesn't the compiler complain that no value is returned here (in case of an error)?
And to be sure, true
means "add constant to the hashtable", false
do not, and otherwise error?
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.
doesn't the compiler complain that no value is returned here
The zend_error_noreturn()
is marked as ZEND_NORETURN
, so the compiler doesn't complain about it.
But yeah, I'm not sure if it's OK or not, since php-src only does the same thing in a few places.
And to be sure, true means "add constant to the hashtable", false do not, and otherwise error?
Yes!
The current form clearly states without redundancy that no value is returned to the caller in this condition, but I am not strongly attached to this part.
Would it be clearer to invert the condition and put the error handling in the if-block so that it returns false on the face of the code, or add an explicit abort in the if-block?
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 the current order is fine, I was just surprised that the compiler didn't complain about not returning in case of an error, and I didn't know how you would handle it with just a boolean (but then you could still reutrn false
as an error should not add the constant anyway)
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've changed the order, as I'm starting to feel that there should be fewer surprises in minor areas.
Zend/zend_inheritance.c
Outdated
static void zend_do_traits_constant_binding(zend_class_entry *ce, zend_class_entry **traits) /* {{{ */ | ||
{ | ||
size_t i; | ||
|
||
for (i = 0; i < ce->num_traits; i++) { | ||
zend_string *constant_name; | ||
zend_class_constant *constant; | ||
|
||
if (!traits[i]) { | ||
continue; | ||
} | ||
|
||
ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&traits[i]->constants_table, constant_name, constant) { | ||
if (do_trait_constant_check(ce, constant, constant_name, traits, i)) { | ||
zend_class_constant *ct = NULL; | ||
|
||
ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant)); | ||
memcpy(ct, constant, sizeof(zend_class_constant)); | ||
constant = ct; | ||
|
||
if (Z_TYPE(constant->value) == IS_CONSTANT_AST) { | ||
ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED; | ||
ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS; | ||
} | ||
|
||
constant->ce = ce; | ||
Z_TRY_ADDREF(constant->value); | ||
constant->doc_comment = constant->doc_comment ? zend_string_copy(constant->doc_comment) : NULL; | ||
if (constant->attributes && (!(GC_FLAGS(constant->attributes) & IS_ARRAY_IMMUTABLE))) { | ||
GC_ADDREF(constant->attributes); | ||
} | ||
|
||
zend_hash_update_ptr(&ce->constants_table, constant_name, constant); | ||
} | ||
} ZEND_HASH_FOREACH_END(); | ||
} | ||
} | ||
/* }}} */ |
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.
Commentary
This function flattens trait constants to the constants table of the composing class.
- The constant definition is copied only if there is no existing constant of the same name.
- The memory area for the copied constants is allocated by the arena allocator and is released at the end of the request.
- Updating ce_flags of the composing class when trait constants hold a constant expression is the same as in interface implementations and inheritances.
- The ce of the copied zend_class_constant is replaced by the class entry of the composing class.
- This is because, unlike interface implementations and class inheritances, access control is done by the scope of the composing class.
- Also, as a result of this, the values, doc comments, and attributes associated with the copied zend_class_constant are destroyed at the time of destroying the class entry of the composing class.
- The handling of doc comments and attributes is in line with the behavior of trait properties.
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.
Other than some minor comments this LGTM
I'll let some time for other people to look at this but will merge before beta 2 is tagged if noone comes allong
Zend/tests/traits/constant_016.phpt
Outdated
|
||
define('CONSTANT', 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.
Have a similar test with a runtime defined constant to see if this borks?
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.
Actually, I chose define()
with the intention of defining a constant at runtime here, but after the optimization of opcache, it seems to compile to the same code as const.
I will use eval to ensure it is defined at runtime.
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.
You could use something from the $_SERVER array
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.
Thanks for pointing this out! I've changed to use $_ENV
Zend/tests/traits/constant_002.phpt
Outdated
class ComposingClass { | ||
use TestTrait1; | ||
use TestTrait3; | ||
public const A = 42; | ||
} |
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.
Isn't it better to test that the composing class works by using two unrelated traits (i.e. a trait A and B where B doesn't use A)? Or is there already such a test case?
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.
Done.
Zend/zend_compile.c
Outdated
if (c->ce->ce_flags & ZEND_ACC_TRAIT) { | ||
return 0; | ||
} else if (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_PUBLIC) { |
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 you inline this comment dirrectly into the code?
Zend/zend_constants.c
Outdated
if (UNEXPECTED(ce->ce_flags & ZEND_ACC_TRAIT)) { | ||
if ((flags & ZEND_FETCH_CLASS_SILENT) == 0) { | ||
zend_throw_error(NULL, "Cannot access trait constant %s::%s directly", ZSTR_VAL(class_name), ZSTR_VAL(constant_name)); | ||
} | ||
goto failure; | ||
} |
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.
Inline this comment into the code please.
Zend/zend_inheritance.c
Outdated
static bool do_trait_constant_check(zend_class_entry *ce, zend_class_constant *trait_constant, zend_string *name, zend_class_entry **traits, size_t current_trait) /* {{{ */ | ||
{ | ||
uint32_t flags_mask = ZEND_ACC_PPP_MASK | ZEND_ACC_FINAL; | ||
|
||
zval *zv = zend_hash_find_known_hash(&ce->constants_table, name); | ||
if (zv == NULL) { | ||
// No existing constant of the same name, so this one can be added | ||
return true; | ||
} | ||
|
||
zend_class_constant *existing_constant = Z_PTR_P(zv); | ||
|
||
if ((ZEND_CLASS_CONST_FLAGS(trait_constant) & flags_mask) == (ZEND_CLASS_CONST_FLAGS(existing_constant) & flags_mask)) { | ||
if (check_trait_property_or_constant_value_compatibility(ce, &trait_constant->value, &existing_constant->value)) { | ||
// There is an existing constant which is compatible with the new one, so no need to add it | ||
return false; | ||
} | ||
} | ||
|
||
// There is an existing constant of the same name, and it conflicts with the new one, so let's throw a fatal error | ||
zend_error_noreturn(E_COMPILE_ERROR, | ||
"%s and %s define the same constant (%s) in the composition of %s. However, the definition differs and is considered incompatible. Class was composed", | ||
ZSTR_VAL(find_first_constant_definition(ce, traits, current_trait, name, existing_constant->ce)->name), | ||
ZSTR_VAL(trait_constant->ce->name), | ||
ZSTR_VAL(name), | ||
ZSTR_VAL(ce->name)); | ||
} | ||
/* }}} */ |
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'm confused here, doesn't the compiler complain that no value is returned here (in case of an error)?
And to be sure, true
means "add constant to the hashtable", false
do not, and otherwise error?
Zend/zend_inheritance.c
Outdated
/* Unlike interface implementations and class inheritances, | ||
* access control of the trait constants is done by the scope | ||
* of the composing class. So let's replace the ce here. | ||
* And the result of this, the values, doc comments, and | ||
* attributes associated with the copied zend_class_constant | ||
* are destroyed at the time of destroying the class entry of | ||
* the composing class. | ||
*/ | ||
constant->ce = ce; |
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.
So basically attributes on trait constants are "useless"? Would it be possible to check at compile time no attributes are used?
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.
So basically attributes on trait constants are "useless"?
Hmm? No, it's not. Maybe my wording is incorrect.
The sentences beginning with "And the result of this" are just to indicate the timing of the destruction of the data associated with the constant. Attributes on trait constants can be retrieved via either the trait itself or the composing class without problems.
For inheritance and interface implementation, the ce of zend_class_constant is kept as it was, while the same zend_class_constant is also inserted into the constants table of the inheriting child.
Destruction of class entries, such as destroy_zend_class(), has a step to check if the ce of zend_class_constant in the constants table matches the class entry about to be destroyed. So the destruction timing of associated data, like attributes or values, is at the time of the destruction of the parent classes or interfaces that define the constants, not at the time of the destruction of the inheriting child. OTH, for trait constants, the destruction timing of each copied zend_class_constant is when each composing class is destroyed.
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.
Thinking again, I decided to remove the part that begins with "And the result of this".
It seems unlikely that this line will be overlooked when someone changes the implementation of access controls of trait constants. But it might be possible when changing the destruction of class entries.
On PR comments, it may now help people read the implementation, but if embedded into the code, it may cause unnecessary misunderstandings in the future. And if one wants to know the timing of destruction, seeing the actual implementation of destructing class entries would be needed anyway.
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.
Removed that part and also added test cases for attributes and doc comments.
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 I got confused because you are talking about destroying the content, when in fact it was already copied into the class the trait is used.
Test failure is unrelated, but could you rebase + force push, as beta 2 has already landed :( I just want to be sure CI mostly works before merging this |
c231bdf
to
b171f84
Compare
@Girgias I just rebased this onto master, should I squash it too? Edit: Squashed it too. |
b171f84
to
0813127
Compare
@Girgias Thanks for reviewing and merging! |
> Constants in Traits > > It is now possible to define constants in traits. This adds a new sniff to detect this. Includes unit tests. Includes docs. Refs: * https://wiki.php.net/rfc/constants_in_traits * https://www.php.net/manual/en/migration82.new-features.php#migration82.new-features.core.constant-in-traits * https://www.php.net/manual/en/language.oop5.traits.php#language.oop5.traits.constants * php/php-src#8888
RFC: https://wiki.php.net/rfc/constants_in_traits
Discussion thread at internals: https://externals.io/message/118039
Voting thread at internals (contains the final discussions): https://externals.io/message/118202
This also closes https://bugs.php.net/bug.php?id=75060