Skip to content

proposal: spec: Compile-time recognition of functions that cannot return normally, using !return #71553

Closed as not planned
@apparentlymart

Description

@apparentlymart

Programming Experience

Go experience: Experienced
Other languages experience: Rust, C, Python, JavaScript

Related Idea

A similar idea was previously proposed as #69591.

Compared to that proposal, this one proposes "non-returning" as a separate characteristic of a function signature independent of its return type, to limit the impact of the change on existing tools, and on package reflect.

This also proposes a different syntax that uses a combination of a punctuation character followed by an existing keyword, rather than introducing a new predeclared identifier.

This proposal does not directly affect error handling, but I am proposing it today primarily for its interaction with #71528, which is an error-handling proposal. In particular, I am interested to find out whether the idea in #71528 might potentially constitute sufficient new information (relative to the discussion in #69591, which was already declined) to reconsider the value in explicitly tracking functions that do not return.

Although I've opened these as separate issues because they are technically independent of one another, in practice I think that this proposal only has potential value if something like #71528 were also accepted. Without that, this proposal is effectively the same as #69591, which was already declined.

Proposal

I propose allowing a new syntax form in the "Result" production of a function or function type declaration, which declares that a function is guaranteed not to return normally. For example:

package os

func Exit(code int) !return {
    // ...
}

!return is pronounced "not return".

When a function is marked in this way, it is prohibited from including any return statements and it must end with a non-return terminating statement.

An unconditional call to a function marked in this way is a new kind of terminating statement.

Because such a function is a terminating statement, the compiler no longer requires an in-practice-unreachable terminating statement after a call to any of these functions to convince the compiler that the end of the function is unreachable. For example:

func raiseNegative() !return {
    panic(errors.New("negative"))
}

func example(v int) int {
    switch {
    case v < 0:
        raiseNegative()
        // Without this proposal, a dynamically-unreachable return or panic would be required here.
    default:
        return v
    }
}

If #71528 were also accepted, and t.FailNow and all of its unconditional callers were modified to be !return, the two proposals would combine to permit test code like the following:

func TestSomething(t *testing.T) {
    got := MightFail() ? err {
        t.Fatalf("unexpected error: %s", err)
        // without _this_ proposal, but under the rules of #71528,
        // a dynamically-unreachable return or panic would be
        // required here.
    }

    // (remainder of the test, presumably using "got")
}

For most type-checking purposes, and for the purposes of all existing APIs for either static analysis or reflection over function types, a !return function is treated as a function whose signature includes no results. !return is an entirely new, separate property of a function type, exposed via new, separate reflection API features. In particular, a function type with !return is assignable to another function type that is identical except for not having !return, although the opposite is not true.

Most existing reflection or static analysis code can ignore this new property and treat such a function as if it returns normally with no results, and thus this change is backward-compatible with existing tools which can treat !return functions just like normal functions that have no results.

The !return property of a function signature affects only what is allowed inside the function's body (as described above) and the function's recognition as a terminating statement when called from elsewhere. Tools that use the "terminating statement" concept for control flow analysis should be updated to recognize !return functions as terminating statements. For example, a tool which detects and warns about unreachable code would begin reporting that any code dominated by a call to a !return function as being unreachable.

Many existing library functions whose documented behavior is consistent with the new !return property have that behavior due to delegating to a lower-level function that has that behavior. For example, testing.T.FailNow delegates to runtime.Goexit. Therefore existing library functions can be gradually improved with !return markings by working outwards from the lowest-level functions. The lowest-level functions will, if implemented in Go themselves, need to include a dynamically-unreachable call to panic as the final non-library-based terminating statement that therefore makes the entire call chain valid as !return. Those which are implemented in assembly language and only declared in Go can have their declaration updated to be !return as long as the assembly implementation guarantees that indeed the function will not return.

Language Spec Changes

Under Function Types, insert the new production NotReturn and redefine Result as follows, leaving all of the other productions unchanged:

NotReturn = "!" "return" .
Result    = Parameters | Type | NotReturn .

Also in that section, add:

A function whose signature includes !return is guaranteed not to return normally once called. All other functions may or may not return normally.

Under Terminating Statements, point 2 changes from "A call to the built-in function panic", to "A call to any !return function".

Under Handling panics, the signature of panic changes to the following, thereby making panic itself a !return function so that it qualifies under the change from the previous paragraph:

func panic(interface{}) !return

Under Function declarations, change the paragraph beginning "If the function's signature declares result parameters", to start instead with "If the function's signature declares result parameters, or !return", and add a new paragraph after its example:

If the function's signature includes !return in the result position, the function's body must not include any return statements, and a return statement is not allowed as its terminating statement.

Informal Change

Most functions return control back to their caller after completing execution, optionally returning one or more values. Some functions may behave in more unusual ways, such as by terminating the entire program. A function marked as !return is special in that it is guaranteed not to return control, and so any statements after it in the program will not be executed.

panic is a !return function that is built into the language. The standard library also includes some !return functions. You can write !return functions in your own program if you wish, although most programs use only the ones from the standard library.

If you write your own !return function then the compiler will return an error unless it can prove that the function does not return. A typical way to ensure that your function does not return is to unconditionally call another, lower-level function that is also marked as !return.

Is this change backward compatible?

Since !return is defined as a tighter constraint on a function's behavior, it is always backward-compatible to add !return to any existing function that returns no results, as long as that function already has no normal return path due to characteristics of its existing implementation.

A function that does not return due to calling another function that does not return cannot be marked as !return until all of its non-returning callees have already been marked as !return, unless the author introduces dynamically-unreachable panic calls or return statements to satisfy the compiler that the function definitely cannot return.

!return is defined as a new, separate property of a function type, as opposed to a new return type, to avoid making breaking changes to package reflect, the static analysis packages, and the tools that use them. Any existing code that is not aware of !return will find what appears to be just a normal function that returns no results. Only tools that actually need to rely on the !return property need be updated to make use of new API features for reporting that.

EDIT: Refer to #71553 (comment) and #71553 (comment) for some further discussion on a backward-compatibility gap that the original proposal text did not identify, and some informal ideas on how it could be addressed by some modifications to the proposal.

Orthogonality: How does this change interact or overlap with existing features?

This change extends the definition of "terminating statement" to include calls to certain user-defined functions. It therefore increases the number of statements that are valid as the last statement in a normal function.

It gives the compiler awareness of a property of a function that is currently not represented in the type system and captured only in human-readable documentation, allowing the compiler to perform more precise analysis and in particular to avoid the need for a redundant, dynamically-unreachable terminating statement after a call to a function that is guaranteed by its documentation to never return.

Although this is only a proposed rather than existing feature, it also addresses one of the ergonomic gaps in #71528, again by allowing the compiler to perform more precise analysis of the control flow within an exceptional block and thus avoid a redundant, dynamically-unreachable terminating statement.

Since I've defined !return as a property of a function type, it is technically possible to declare a variable that can only have !return functions assigned to it. However, it's unclear to me whether that's actually useful in practice.

Would this change make Go easier or harder to learn, and why?

Since there are various functions in the standard library that are already effectively !return per their documentation, most Go programmers can continue to rely on the documentation to learn about this characteristic and so would not need to directly learn about !return in order to successfully write most Go code, unless they want to provide a function that has that characteristic itself.

Several other programming languages with similar characteristics to Go have a feature similar to this. C23 and C++11 both represent this using a function attribute, although those languages do not actually prevent such a function from returning: it's simply undefined behavior to return. Rust includes the "never" type, spelled !, which represents this characteristic as a type that has no values and thus cannot be constructed to return it.

As with those other languages, I expect this feature would be used primarily by standard library features that interact with the runtime or the operating system, and so most Go application or library developers would not learn about it at all.

Some new Go developers might find it helpful for their tools to indicate when a !return function makes a later statement effectively unreachable, thereby avoiding them having to refer to the function's documentation to learn that. However, I expect that benefit is marginal.

Cost Description

This change introduces yet another property to track about a function type, and a new assignability rule for function types that differ only in !return. Function types are already quite complicated.

The runtime type representation used by reflect would need to use another bit of abi.Type.TFlag to represent the presence of !return. This flag applies only to function types, which is not true for any other bit of that field that's currently allocated, and that's potentially confusing. (Other implementations are possible, but this one avoids growing the overall size of the runtime type representation.)

A function marked as !return will often require at least one of its callees to also be marked as !return, and so although there is no requirement to immediately add !return to all existing non-returning functions in the ecosystem there would still likely be pressure from higher-level library authors on lower-level library authors to add !return annotations, so that the higher-level authors can avoid introducing redundant dynamically-unreachable terminating statements to satisfy the compiler that a function is indeed !return. In turn, users of those higher-level libraries may pressure the library authors to add !return to existing functions so that they can be used ergonomically as the terminating statement of a function or in an exception block.

Changes to Go ToolChain

Any tool that attempts to detect and report unreachable code should be changed to reflect the new definition of "terminating statements", which in turn means becoming able to check whether all callees are !return. This includes the unreachable check in go vet.

All tools that include a Go source code parser must learn to treat !return as valid in the function signature result position, rather than reporting it as a syntax error.

Performance Costs

The main additional cost would be felt at compile-time, in the code which verifies whether a function ends with a terminating statement, and in the new code which verifies that a !return function definitely does not return.

!return has no significant runtime performance impact, since all of the checks related to it should complete at compile time. At runtime, a !return function is exactly equivalent to a function that returns no results, except that the control flow instructions in the generated code will definitely not include something equivalent to ret.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions