====== PHP RFC: noreturn type ====== * Version: 0.1 * Date: 2021-03-10 * Author: Matt Brown & Ondřej Mirtes * Status: Implemented * Proposed Version: PHP 8.1 * Implementation: https://github.com/php/php-src/pull/6761 ===== Introduction ===== There has been a trend over the past few years that concepts initially just expressed in PHP docblocks eventually become native PHP types. Past examples are: [[https://wiki.php.net/rfc/scalar_type_hints_v5|scalar typehints]], [[https://wiki.php.net/rfc/return_types|return types]], [[https://wiki.php.net/rfc/union_types_v2|union types]], [[https://wiki.php.net/rfc/mixed_type_v2|mixed types]], and [[https://wiki.php.net/rfc/static_return_type|static types]]. Our static analysis tools currently provide support for '' @return noreturn '' docblocks to denote functions that always ''throw'' or ''exit''. Users of these tools have found that syntax useful to describe the behaviour of their own code, but we think it’s even more useful as a native return type, where PHP compile-time and runtime type-checks can guarantee its behaviour. ===== Proposal ===== Introduce a ''noreturn'' type that can be used in functions that always ''throw'' or ''exit''. Redirect functions that always call ''exit'' (either explicitly or implicitly) are good candidates for such a return type: function redirect(string $uri): noreturn { header('Location: ' . $uri); exit(); } function redirectToLoginPage(): noreturn { redirect('/login'); } PHP developers can call these functions, safe in the knowledge that no statements after the function call will be evaluated: function sayHello(?User $user) { if (!$user) { redirectToLoginPage(); } echo 'Hello ' . $user->getName(); } If, at some later date, the redirect function is changed so that it does //sometimes// return a value, a compile error is produced: function redirect(string $uri): noreturn { if ($uri === '') { return; // Fatal error: A noreturn function must not return } header('Location: ' . $uri); exit(); } If, instead, the above function is rewritten to have an //implicit// return, a ''TypeError'' is emitted: function redirect(string $uri): noreturn { if ($uri !== '') { header('Location: ' . $uri); exit(); } } redirect(''); // Uncaught TypeError: redirect(): Nothing was expected to be returned Attempting to use ''yield'' inside a ''noreturn'' function produces a compile-time error: function generateList(string $uri): noreturn { yield 1; exit(); } // Fatal error: Generator return type must be a supertype of Generator ==== Applicability ==== Like ''void'', the ''noreturn'' type is only valid when used as a function return type. Using ''noreturn'' as an argument or property type produces a compile-time error: class A { public noreturn $x; // Fatal error } ==== Variance ==== In type theory ''noreturn'' would be called a "bottom" type. That means it's effectively a subtype of every other type in PHP’s type system, including ''void''. It obeys the rules you might expect of a universal subtype: Return type covariance is allowed: 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'); } } Return type contravariance is prohibited: abstract class Redirector { abstract public function execute(): noreturn; } class BadRedirector extends Redirector { public function execute(): void {} // Fatal error } Returning by reference with a ''noreturn'' type is allowed as well. class A {     public function &test(): int { ... } } class B extends A {     public function &test(): noreturn { throw new Exception; } } Returning ''noreturn'' is also allowed in %%__%%toString methods: class A implements Stringable { public function __toString(): string { return "hello"; } } class B extends A { public function __toString(): noreturn { throw new \Exception('not supported'); } } === Allowed return types when a function always throws === Since ''noreturn'' is a subtype of all other types, a function that //could// be annotated with ''noreturn'' can still safely be annotated with another return type: function doFoo(): int { throw new \Exception(); } ==== Prior art in other interpreted languages ==== * Hacklang has a [[https://docs.hhvm.com/hack/built-in-types/noreturn|noreturn type]]. * TypeScript has a [[https://www.typescriptlang.org/docs/handbook/basic-types.html#never|never type]] that's also an explicit bottom type. * Python has a [[https://docs.python.org/3/library/typing.html#typing.NoReturn|NoReturn type]] as part of its official typing library. ==== Prior art in PHP static analysis tools ==== In the absence of an explicit return type some PHP static analysis tools have also adopted support for ''noreturn'' or similar: * Psalm and PHPStan support the docblock return type ''/** @return noreturn */'' * PHPStorm supports a custom PHP 8 attribute ''#[JetBrains\PhpStorm\NoReturn]'' ==== Comparison to void ==== Both ''noreturn'' and ''void'' are both only valid as return types, but there the similarity ends. When you call a function that returns ''void'' you generally expect PHP to execute the next statement after that function call. function sayHello(string $name): void { echo "Hello $name"; } sayHello('World'); echo ", it’s nice to meet you"; But when you call a function that returns ''noreturn'' you explicitly do not expect PHP to execute whatever statement follows: function redirect(string $uri): noreturn { header('Location: ' . $uri); exit(); } redirect('/index.html'); echo "this will never be executed!"; ==== Attributes vs types ==== Some might feel that ''noreturn'' belongs as a function/method attribute, potentially a root-namespaced one: Attribute form: #[\NoReturn] function redirectToLoginPage(): void {...} Type form: function redirectToLoginPage(): noreturn {...} We believe it’s more useful as a type. Internally PHP has a much more straightforward interpretation of return types than attributes, and PHP can quickly check variance rules for ''noreturn'' types just as it does for ''void''. It's also tidier. ==== Naming ==== Naming is hard. We each have different preferences. Arguments for ''noreturn'': * Very unlikely to be used as an existing class name. * Describes the behaviour of the function. Arguments for ''never'': * It's a single word - ''noreturn'' does not have any visual separator between the two words and one cannot be sensibly added e.g. ''no-return''. * It's a full-fledged type, rather than a keyword used in a specific situation. A far-in-the-future generics proposal could use ''never'' as a placeholder inside [[https://docs.hhvm.com/hack/built-in-types/nothing#usages|contravariant generic types]]. ===== Backwards Incompatible Changes ===== ''noreturn'' becomes a reserved word in PHP 8.1 ===== Proposed PHP Version(s) ===== 8.1 ===== Patches and Tests ===== Draft implementation here: https://github.com/php/php-src/pull/6761 ===== Vote ===== Voting opens 2021-03-30 and 2021-04-13 at 11:00:00 AM EDT. 2/3 required to accept. * Yes * No Following vote requires simple majority: * noreturn * never