Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_type
The feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.
Thanks,
Matt & Ondřej
Hey Matthew,
I have look at the implementation a few days ago, and was patiently waiting for the RFC.
a huge +1 from me ( even tho i can't vote ), noreturn
type would be a great addition to PHP type system.
Regards,
Saif.
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.Thanks,
Matt & Ondřej
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_type
Thanks for the proposal, I can certainly think of uses for it.
I am however slightly confused by what exactly the implementation
checks, and when. Is it actually looking for "exit" and "throw" statements?
The text talks about exiting "explicitly or implicitly", but doesn't
give any examples of how this works. For instance, is this code valid,
and how does PHP know that it's valid?
function my_exit(string $reason): noreturn {
exit("$reason\n");
}
function canned_exit(): noreturn {
my_exit("The normal reason");
}
The other thing I'd like to mention is that PHP's implementation of
"void" consists mostly of checking that there is no return statement, so
a separate keyword of "noreturn" may well cause confusion. I would
personally prefer the name "never" for this reason.
Regards,
--
Rowan Tommins
[IMSoP]
I am however slightly confused by what exactly the implementation
checks, and when. Is it actually looking for "exit" and "throw" statements?
No. Any function annotated with : noreturn
causes the engine to insert
a ZEND_VERIFY_NORETURN_TYPE op at the end of the function's operands.
If the Zend engine hits that operand — which only happens if a throw/exit
hasn't been encountered — it emits a TypeError.
The other thing I'd like to mention is that PHP's implementation of
"void" consists mostly of checking that there is no return statement, so
a separate keyword of "noreturn" may well cause confusion. I would
personally prefer the name "never" for this reason.
If a significant number agree I can add a secondary vote on noreturn vs
never, but never introduces more of a BC risk.
Best wishes,
Matt
On Wed, Mar 10, 2021 at 2:22 PM Matthew Brown matthewmatthew@gmail.com
wrote:
On Wed, 10 Mar 2021 at 13:55, Rowan Tommins rowan.collins@gmail.com
wrote:I am however slightly confused by what exactly the implementation
checks, and when. Is it actually looking for "exit" and "throw"
statements?No. Any function annotated with
: noreturn
causes the engine to insert
a ZEND_VERIFY_NORETURN_TYPE op at the end of the function's operands.If the Zend engine hits that operand — which only happens if a throw/exit
hasn't been encountered — it emits a TypeError.The other thing I'd like to mention is that PHP's implementation of
"void" consists mostly of checking that there is no return statement, so
a separate keyword of "noreturn" may well cause confusion. I would
personally prefer the name "never" for this reason.If a significant number agree I can add a secondary vote on noreturn vs
never, but never introduces more of a BC risk.
I think the difference between void and noreturn is pretty clear, but, if
never is more clear, perhaps neverreturn would be an option that would be
less likely to have BC risks.
Just to throw out some additional ideas, why not two possible types: throws
and exits? Don't have any strong opinions on that, but figured I'd add it
to the discussion.
Best wishes,
Matt
--
Chase Peeler
chasepeeler@gmail.com
If the Zend engine hits that operand — which only happens if a
throw/exit hasn't been encountered — it emits a TypeError.
Right, that should probably be spelled out in the RFC. Checking at
run-time in that way is consistent with actual return types, but
inconsistent with "void", which performs all its checking at compile-time.
On that note, the "comparison to void" section could maybe more clearly
call out the difference in behaviour, rather than showing the happy path
for both. If I understand right:
- the "sayHello" function would give an error at runtime if marked
"noreturn" - the "redirect" function would run fine if marked "void"
- a function containing "return null;" would fail at compile time for both
- a function containing "return;" would be OK for "void", but fail at
compile time for "noreturn"?
Regards,
--
Rowan Tommins
[IMSoP]
On Wed., Mar. 10, 2021, 11:22 Matthew Brown, matthewmatthew@gmail.com
wrote:
If a significant number agree I can add a secondary vote on noreturn vs
never, but never introduces more of a BC risk.
Hi Matt,
I like this RFC, but I'd like to see the RFC cover if any other languages
have a similar return type.
The definition of void
is that it has no return value, so I too agree
that the keyword noreturn
is too close in meaning to void
.
I'd also like to throw the word deadend
or something similar into the
ring, to make things a bit clearer.
Thanks,
Peter
Am 11.03.2021 um 07:51 schrieb Peter Stalman:
I like this RFC, but I'd like to see the RFC cover if any other languages
have a similar return type.
The RFC has a "Prior art in other interpreted languages" section.
Am 11.03.2021 um 07:51 schrieb Peter Stalman:
I like this RFC, but I'd like to see the RFC cover if any other languages
have a similar return type.The RFC has a "Prior art in other interpreted languages" section.
Oh, my apologies, I missed that section. I admit I (obviously) didn't read
the RFC as diligently as I should have.
I'd also like to suggest terminus
, as it seems to fit by definition: "a
final point in space or time; an end or extremity" and "a final goal; a
finishing point".
It might also be less likely to conflict with any existing code.
Thanks,
Peter
Hi Peter and internals
On Wed., Mar. 10, 2021, 11:22 Matthew Brown, matthewmatthew@gmail.com
wrote:If a significant number agree I can add a secondary vote on noreturn vs
never, but never introduces more of a BC risk.Hi Matt,
I like this RFC, but I'd like to see the RFC cover if any other languages
have a similar return type.
What made it clear to me was this:
returning nothing (void) isn't the same as not returning (noreturn, eg throw or exit).
Having read that somewhere on Twitter made it easier for me to reason about. It's a useful feature in the static analysis world.
The definition of
void
is that it has no return value, so I too agree
that the keywordnoreturn
is too close in meaning tovoid
.I'd also like to throw the word
deadend
or something similar into the
ring, to make things a bit clearer.Thanks,
Peter
Kind regards
Brent
Hi Rowan,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThanks for the proposal, I can certainly think of uses for it.
The other thing I'd like to mention is that PHP's implementation of
"void" consists mostly of checking that there is no return statement, so
a separate keyword of "noreturn" may well cause confusion. I would
personally prefer the name "never" for this reason.
I would mostly say that "void" consists of checking that no value is
returned as plain return;
statements are fine.
While actually "noreturn" consists of checking that there is no return
statement, implicit or explicit.
As another distinction I noticed, for "void" the validation can be done
completely at compile time but for "noreturn" both compile and run time are
necessary right now.
Hopefully, static analysis tools like psalm can further understand better
and better the runtime execution paths and provide with errors.
I was just reading the github committed RFC earlier this morning and I
liked it.
Regards,
Alex
Hi Matthew,
I am concerned with some edge cases. What if a function both returns and
throws at the same time? For example:
function a():noreturn {
return throw new Exception('Boom!');
}
or
function a():noreturn {
try {
throw new Exception('Boom!');
} finally {
return;
}
}
Also, the message "TypeError: a(): Nothing was expected to be returned" is
inaccurate. It should say that the function should terminate instead of
returning. It's also not a TypeError if no value is expected. At the moment
it sounds like we need to make the function somehow return nothing.
If we are to bikeshed about the name then we might also consider other
possibilities: None, Nothing, Returnless
Regards,
Kamil
I am concerned with some edge cases. What if a function both returns and
throws at the same time? For example:function a():noreturn {
return throw new Exception('Boom!');
}or
function a():noreturn {
try {
throw new Exception('Boom!');
} finally {
return;
}
}
In both cases those are compile-time errors, since no return is allowed in
a function that has the noreturn type.
I've added both as tests (
https://github.com/php/php-src/pull/6761/commits/56ae52e84da5c063c28ba7739ac15979e61afc91)
but I don't think it's necessary to mention it in the PR given how much
of an edge-case it is.
Also, the message "TypeError: a(): Nothing was expected to be returned" is
inaccurate. It should say that the function should terminate instead of
returning. It's also not a TypeError if no value is expected. At the moment
it sounds like we need to make the function somehow return nothing.
It's a TypeError because noreturn is a type, but I agree the error message
could be improved.
Ilija suggested "noreturn function was expected to throw, terminate or run
infinitely" and I'd welcome other suggestions.
If we are to bikeshed about the name then we might also consider other
possibilities: None, Nothing, Returnless
I'd prefer to stick to noreturn and never, which have plenty of precedent
in other languages' type systems.
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_type
I don't like that type covariance would be allowed. Why such an
exception to the rules?
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeI don't like that type covariance would be allowed. Why such an
exception to the rules?
It’s not an exception. Returns are covariant. Parameters are
contravariant. Since noreturn
is a subtype of all other types, it
behaves as expected.
I like the proposal, btw!
Cheers,
Ben
I don't like that type covariance would be allowed. Why such an
exception to the rules?It’s not an exception. Returns are covariant. Parameters are
contravariant. Sincenoreturn
is a subtype of all other types, it
behaves as expected.
I see it's a subtype, I don't get why. Wouldn't it be better to be a
separate type so return type covariance is not allowed (as it is with
void)? Was it a design decision or a side product of the implementation?
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Hi Aleksander
I see it's a subtype, I don't get why. Wouldn't it be better to be a
separate type so return type covariance is not allowed (as it is with
void)? Was it a design decision or a side product of the implementation?
This is a fairly common technique. For example, this is from the
TypeScript handbook:
https://www.typescriptlang.org/docs/handbook/basic-types.html#never
The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself). Even any isn’t assignable to never.
Imagine the following scenario:
function foo(callable(): int $computeSomething) {
$magicNumber = $computeSomething();
}
function bar(): noreturn {
throw new Exception('bar');
}
// Is completely type-safe as $magicNumber will never be assigned
foo('bar');
// Without noreturn being a bottom type you'd have to do this
foo(function(): int {
bar();
});
We don't have callable types yet but it's common in other languages.
The point is that, since noreturn will throw, terminate or never end,
$magicNumber will never be assigned. This means effectively the return
type of any function can be safely substituted with noreturn.
The other benefit is that you can accurately typehint an overridden method.
class Foo {
function baz(): int {
return 42;
}
}
class Bar extends Foo {
function baz(): noreturn {
throw new Exception();
}
}
$bar = new Bar();
$bar->baz(); // IDE will know this always terminates
qux(); // Dead code
Not every language allows this but it does make sense semantically.
It's not a deal breaker if we don't have it but given that the
implementation is fairly simple I don't see why we wouldn't.
Ilija
Hey Aleksander,
I don't like that type covariance would be allowed. Why such an
exception to the rules?It’s not an exception. Returns are covariant. Parameters are
contravariant. Sincenoreturn
is a subtype of all other types, it
behaves as expected.I see it's a subtype, I don't get why. Wouldn't it be better to be a
separate type so return type covariance is not allowed (as it is with
void)? Was it a design decision or a side product of the implementation?
noreturn
is what's called the "bottom type", and it's a subtype of all
types.
It can sound counter-intuitive, but it is true that all methods in a
subclass can be re-implemented with the bottom type as their return type
(instead of their original one), and the system is still sound from a type
perspective.
Marco Pivetta
Am 10.03.2021 um 19:06 schrieb Matthew Brown:
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_type
Thank you, Ondřej and Matt, for bringing this up. Makes sense to me, +1.
Hi Matt & Ondřej,
One of the things that makes PHP different from other languages (and
for better for a change) is that every function returns a value, even
where there is no explicit return statement.
This eliminates a large number of edge-cases in code like:
function log_result(mixed $bar) {...}
log_result($anyCallable());
is always valid in PHP, whereas in other languages similar code can
either refuse to compile, or error out when run, or other things like
'undefined' variables.
I.E. in other languages, there are three exit conditions:
- no value returned
- a value returned
- function has no normal exit
And so other languages need to indicate between 'void' and 'no return'
But in PHP there are only two exit conditions*:
- a value returned
- function has no normal exit
I (and others) brought this up during the void RFC:
https://news-web.php.net/php.internals/88990 and said that null was
the right choice over void, as it matches what the language actually
does.
That would have left void available to mean 'this function' does not
exit normally.
Unfortunately people in the community have started doing what I
feared, and using void as 'no value is returned', which is not what
the language actually does.
I realise the above might be slightly discombobulating if, for
example, some people had written large static code analyzers that had
misinterpreted void like this.
I think we should introduce null. But we shouldn't introduce
'noreturn' even if a large number have misunderstood what the
behaviour of the language is. It might be appropriate for the other
languages that are listed in the other language section, but it's not
appropriate in PHP which is different to those languages.
cheers
Dan
Ack
- so the usage of null and void return types should be:
function returns(): null {
if (rand(0, 100) === 0) { throw new \Exception('Surprise!');}
// intentionally no code
}
function never_returns(): void {
if (rand(0, 100) === 0) { throw new \Exception('Surprise!');}
while (1) {}
}
For both of them, there are exceptions to the declared return type,
but otherwise 'returns' returns the value null, and 'never_returns'
never returns a value.
Hi Dan,
While I have some sympathy for your first point:
I (and others) brought this up during the void RFC:
https://news-web.php.net/php.internals/88990 and said that null was
the right choice over void, as it matches what the language actually
does.
I disagree with your second, which doesn't follow at all:
That would have left void available to mean 'this function' does not
exit normally.
Using "void" for this would be even more confusing than what we have now. If anything, "void" should have been reserved for an implementation that checked correct usage at the call site, as happens in other languages. Although, if someone can come up with an implementation of that, adding it with a suitable lead in time would still be possible.
It's also something of a moot point, since even if we introduced a null return type (which would be a subtly different feature from void as currently implemented), we couldn't suddenly repurpose a keyword that's already used. So if you think "never returns" is something worth representing, it will need a new keyword, several options for which have been proposed.
Regards,
--
Rowan Tommins
[IMSoP]
Unfortunately people in the community have started doing what I
feared, and using void as 'no value is returned', which is not what
the language actually does.I realise the above might be slightly discombobulating if, for
example, some people had written large static code analyzers that had
misinterpreted void like this.
I think it is you that is misinterpreting things. The "void" keyword explicitly signals the intent "this function does not return a value". People find that a useful thing to signal, even if the language still allows the function to be used in expression context. They also find it useful to be reminded "Hey, you know this function you're using as an expression? That makes no sense, because it doesn't return anything."
You may not find that useful, but those that do are using "void" exactly as intended.
Regards,
Oh, and on this point:
Rowan Tommins
[IMSoP]
Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.Thanks,
Matt & Ondřej
Hi Matt & Ondřej,
I wanted to give my +1 to this proposal.
I was curious to see how fibers might interact with this declaration, since it is possible to create a fiber that can not return. So, I compiled your branch and gave it a try.
$fiber = new Fiber(function (): noreturn {
while (true) {
Fiber::suspend(\random_int(0, 100));
}
});
$result = $fiber->start();
for ($i = 0; $result; ++$i) {
echo $result, "\n";
$result = $fiber->resume();
}
echo "Generated ", $i, " numbers before generating zero.\n";
This short script works just as expected, cool! :-D
Cheers,
Aaron Piotrowski
On Wed, Mar 10, 2021 at 7:07 PM Matthew Brown matthewmatthew@gmail.com
wrote:
Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.Thanks,
Matt & Ondřej
Thanks for the proposal! I think this is a nice addition.
Regards,
Nikita
On Wed, Mar 10, 2021 at 7:07 PM Matthew Brown matthewmatthew@gmail.com
wrote:
Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.Thanks,
Matt & Ondřej
Is it allowed to declare a noreturn function that returns by reference?
function &foo(): noreturn {}
Regards,
Nikita
Hey Nikita,
Is it allowed to declare a noreturn function that returns by reference?
function &foo(): noreturn {}
Given that noreturn
means it should throw, loop forever or exit, how
would a by-ref usage be applied/useful?
Or is it a hint at a missing test?
Hey Nikita,
Is it allowed to declare a noreturn function that returns by reference?
function &foo(): noreturn {}
Given that
noreturn
means it should throw, loop forever or exit, how
would a by-ref usage be applied/useful?Or is it a hint at a missing test?
Mainly a hint for missing spec ;)
Context is that we're considering to deprecate by-ref void return (
https://wiki.php.net/rfc/deprecations_php_8_1#return_by_reference_with_void_type),
so it would make sense to me to prohibit this for noreturn from the start.
However, I could also see an argument for why allowing it may be useful due
to variance considerations. It would allow you to write something like this:
<?php
class A {
public function &test(): int { ... }
}
class B extends A {
public function &test(): noreturn { throw new Exception; }
}
While dropping the by-ref return on B::test() would be forbidden by
variance (and I don't think we'd want to add more special rules here, like
ignoring ref-return variance for noreturn functions).
Regards,
Nikita
Hey Nikita,
Is it allowed to declare a noreturn function that returns by reference?
function &foo(): noreturn {}
Given that
noreturn
means it should throw, loop forever or exit, how
would a by-ref usage be applied/useful?Or is it a hint at a missing test?
Mainly a hint for missing spec ;)
Context is that we're considering to deprecate by-ref void return (
https://wiki.php.net/rfc/deprecations_php_8_1#return_by_reference_with_void_type),
so it would make sense to me to prohibit this for noreturn from the start.However, I could also see an argument for why allowing it may be useful
due to variance considerations. It would allow you to write something like
this:<?php
class A {
public function &test(): int { ... }
}
class B extends A {
public function &test(): noreturn { throw new Exception; }
}While dropping the by-ref return on B::test() would be forbidden by
variance (and I don't think we'd want to add more special rules here, like
ignoring ref-return variance for noreturn functions).Regards,
Nikita
I think it should be allowed due to the variance considerations – I've
updated the RFC to include your example.
Hey,
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.Thanks,
Matt & Ondřej
My main concern is around the covariance of inheritance. The example given:
abstract class Person
{
abstract public function hasAgreedToTerms(): bool;
}
class Kid extends Person
{
public function hasAgreedToTerms(): noreturn
{
throw new \Exception('Kids cannot legally agree to terms');
}
}
While that may be technically correct from a type theory perspective (my type theory isn't strong enough to say either way), I don't feel like that obeys Liskov. If I have an array of Person objects, and I iterate them to check if they've all agreed to terms, I expect the return value to be a usable type in the Person interface or a subtype of it that I can still use. I cannot use Kid's return type, because it's by definition non-existent.
That feels like a bad time bomb.
Other than that concern, I'm fine with the spec. I would marginally prefer "never" over "noreturn", but not enough to vote against it just for that.
--Larry Garfield
My main concern is around the covariance of inheritance. The example
given:abstract class Person
{
abstract public function hasAgreedToTerms(): bool;
}class Kid extends Person
{
public function hasAgreedToTerms(): noreturn
{
throw new \Exception('Kids cannot legally agree to terms');
}
}While that may be technically correct from a type theory perspective (my
type theory isn't strong enough to say either way), I don't feel like that
obeys Liskov.
It absolutely obeys Liskov, because noreturn is the subtype of all
subtypes, aka the bottom type.
If I have an array of Person objects, and I iterate them to check if
they've all agreed to terms, I expect the return value to be a usable
type in the Person interface or a subtype of it that I can still use. I
cannot use Kid::hasAgreedToTerms's return type, because it's by definition
non-existent.
Luckily your program will never have the chance to use its return type,
because calling the method throws an exception.
That feels like a bad time bomb.
You might, in this scenario, add a @throws
docblock and hope some sort of
static analysis rule might catch the time bomb
Another way that you can think of this is with throw expressions. The
ternary in the function below is supposed to return string
, or a subtype
of string
(the literal string "hello" is a subtype of string
). The
types are valid here if the type of throw new Exception...
is treated as
noreturn
, and if noreturn
is a subtype of string
.
<?php
function getString(): string {
return rand(0, 1) ? "hello" : throw new \Exception('bad');
}
Other than that concern, I'm fine with the spec. I would marginally
prefer "never" over "noreturn", but not enough to vote against it just for
that.
There will be a separate vote for noreturn vs never!
--Larry Garfield
--
To unsubscribe, visit: https://www.php.net/unsub.php
Hi Matthew
Ondřej Mirtes and I present an RFC for the noreturn type:
https://wiki.php.net/rfc/noreturn_typeThe feature already exists in Hack (the primary inspiration) and is
currently supported by our static analysis tools inside docblocks, and we
feel there's a good argument for it to be supported by PHP itself.
Thanks for the RFC! I'm very much in support of it.
Two small things:
-
Some magic methods like __toString currently require a specific
return type (like string in that case). Since noreturn is a bottom
type technically it should be possible to type hint those magic
methods with noreturn. It's not a big issue if that's not possible,
but it should be mentioned in the RFC. -
noreturn is one of the few return types that would technically make
sense for __construct (other than void).
class Foo {
public function __construct(): noreturn {
throw new Exception();
}
}
new Foo();
bar(); // < Dead code
Not sure this is worth supporting but I just wanted to mention it.
Ilija