-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Add covariance/contravariance to inherited methods #3732
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
1c6bea2
to
20dbef1
Compare
20dbef1
to
039f71b
Compare
This currently doesn't warn, even though it should:
(What I really wanted to do here is show the opposite -- I don't think your code handles classes split across namespaces correctly, i.e. it will not verify variance for all of them together.) |
Zend/tests/typehints/undefined_type_during_variance_check_04.phpt
Outdated
Show resolved
Hide resolved
Zend/zend_compile.c
Outdated
uint32_t i; | ||
for (i = 0; i < list->children; ++i) { | ||
zend_compile_top_stmt(list->child[i]); | ||
zend_ast **begin = list->child; |
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.
As already mentioned, this code is not going to handle namespaces correctly, especially when using braced namespace Foo { ... }
form. You'll want classes that are part of multiple braced (or unbraced, but braced is the more complex case) to be part of the same variance region.
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 finally understand what you are saying:
namespace NS1 {
class A implements B {}
}
namespace NS2 {
class X implements Y {}
}
The A
should be verified after X
is declared. Right?
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.
Yes.
Zend/zend_inheritance.c
Outdated
|
||
/* by-ref constraints on arguments are invariant */ | ||
if (fe_arg_info->pass_by_reference != proto_arg_info->pass_by_reference) { | ||
if (!_check_inherited_parameter_type(fe, fe_arg_info, proto, proto_arg_info)) { |
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.
Shouldn't this be passing on the -1
state as well?
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.
Or maybe not? I don't really get how or at which point this code interacts with CG(unverified_types)
. I expected that something would remove a class from that set if verification returns 1 (rather than -1), but don't see it.
Perhaps I made a mistake when putting this into a form that would work in a single file test (or maybe the issue is really when it is split across files), but this fatals for me: namespace Foo {
class A {
function foo(B $x): A {
return new self();
}
}
}
namespace Bar {
class B extends \Foo\A {
function foo(\stdClass $x): B {
return new self();
}
}
}
namespace {
var_dump((new \Bar\B())->foo(new \stdClass()));
} I get:
This is because |
@morrisonlevi Just checked again, the code I provided (exactly as-is, single file) does not produce an error for me. Your code does error. I guess the different namespace style (semicolon vs brace) matters here. |
I verified that in your example it does not emit |
zend_ast_list *list = zend_ast_get_list(ast); | ||
zend_ast **begin = list->child; | ||
zend_ast **end = begin + list->children; | ||
zend_ast_list *const list = zend_ast_get_list(ast); |
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.
nit: We don't use postfix const (at least for locals).
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.
This deals with two ranges, and I kept accidentally moving begin
. I slapped const on a few variables so that the compiler would catch my mistakes here. I can remove it if we really care.
/* Compile any stmts between the two type decls (or the end) */ | ||
for (p = last_decl; p < first_decl; ++p) { | ||
zend_compile_stmt(*p); | ||
} |
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.
Should this really be here? Our early binding only applies to top-level class declarations, not conditional ones, so I would kind of expect that this also holds for variance checks. On the other hand, I don't see a pressing technical reason why we can't do this.
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.
Maybe there is a better way, but this fixed the bug you posted, the one with multiple namespaces without using braces. We need to support variance, even if it's not top-level. Here's another example:
<?php
interface A {
function m(X $z);
}
if (true) {
class B implements A {
function m(Y $z) {} // ?
}
}
We have a test case for things like this, but I forgot which one it was off the top of my head.
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.
While we do need to support variance checks for it (we certainly need that for everything), what (I think, maybe) we don't need it support is the ability to verify variance for multiple classes at once (with cyclic dependencies). I don't think we can reasonable check variance for both classes together in the example you provided, we'd have to check first A and then B, independently.
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 verified that the variance opcodes are delayed correctly for this code:
<?php
interface A {
function m(X $z): A;
}
if (true) {
class B implements A {
function m(X $z): B {
return new self();
}
}
class C extends B {
function m(X $z): C {
return new self();
}
}
}
Snippet of opcodes:
L7 #0 JMPZ true J5
L8 #1 DECLARE_CLASS "b"
L13 #2 DECLARE_INHERITED_CLASS "c" "B"
L13 #3 VERIFY_VARIANCE "b"
L13 #4 VERIFY_VARIANCE "c"
L21 #5 RETURN<-1> 1
Maybe I misunderstood your point, but it seems to work.
I'm wondering whether this case is covered by this RFC : <?php declare(strict_types=1);
interface TaskInterface {}
interface TaskHandlerInterface {
public function handle(TaskInterface $task): void;
}
class TaskHandler implements TaskHandlerInterface {
public function handle(TaskInterface $task): void {
/* ... */
}
}
class MultipleTaskHandler implements TaskHandlerInterface {
private $handler;
public function __construct(TaskHandlerInterface $handler) {
$this->handler = $handler;
}
/**
* Not permitted, but type-safe :/
*/
public function handle(TaskInterface ...$tasks): void {
foreach($tasks as $task) {
$this->handler->handle($task);
}
}
} |
@azjezz I don't think that example is related to contravariance, since it doesn't change the type of the parameter but rather the number of parameters.
|
Fewer should be fine, as long as the same number can still be accepted. More should be fine, as long as they are optional. Unless I'm just really tired, the behavior should be permissible. |
Return types are covariant; parameter types are contravariant.
This should probably be moved somewhere else and renamed.
This work is based off Nikita's review. Notably: - Implement contravariance in terms of covariance - Improve support for non-consecutive type decls - Adds _inheritance_status enum
9d3a948
to
cd7d1a8
Compare
cd7d1a8
to
bf4e912
Compare
Hi @morrisonlevi . Thank you for your great work on this. I am waiting in eager anticipation for this to land, and I wonder if you could please give a rough estimate of when it will be ready? Anyway, keep up the good work, I appreciate it. |
I've just reverted the compile-time parent check in deb44d4, because it breaks Mockery. Due to the issue with parent in traits this patch needs to deal with unresolved parent anyway, so I don't think this is problematic. @davedevelopment As a heads up, it would be good to not generate this code https://github.com/mockery/mockery/blob/4324afeaf9d95b492507e6587abb3f024e2576de/library/Mockery/Mock.php#L603 if there is no parent class, as I expect we'll want to make it a compile-error at some point, even if not in 7.4. |
@TerjeBr The RFC was accepted for PHP 7.4 so unless some serious problem appears it can be expected to land there. |
A fun failing case: <?php
$b = new B;
echo "foobar\n";
return;
class A {
public function test(): X {}
}
class B extends A {
public function test(): Y {}
} This will happily create the object without ever verifying variance. The reason is that the classes are early-bound, but the verification is still only performed at the point of definition. We'd have to move the verification to the start of the op array or something -- ideally we'd really not early-bind in this case. |
Guys, I can see that return types overloading is implemented correctly in PHP 7.4, but why this code throws the fatal error?
|
@madonzy Please see https://en.wikipedia.org/wiki/Liskov_substitution_principle and https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) for more information. |
@madonzy Arguments to methods are contravariant: |
Hi guys, In the documentation it says:
Then how come the following throws an error:
Thanks! |
Return types are covariant; parameter types are contravariant.
RFC: https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters
Replaces PR #3712.