Skip to content

Latest commit

 

History

History
369 lines (274 loc) · 17.1 KB

DIP1038.md

File metadata and controls

369 lines (274 loc) · 17.1 KB

@mustuse

Field Value
DIP: 1038
Review Count: 2
Author: Paul Backus (snarwin@gmail.com)
Implementation: dlang/dmd#13589
Status: Accepted

Abstract

This DIP proposes a new attribute, @mustuse, which can be applied to a struct or union type to make ignoring an expression of that type into a compile-time error. It can be used to implement alternative error-handling mechanisms for code that cannot use exceptions, including @nogc and BetterC code.

Contents

Rationale

Error handling without exceptions

Currently in D, the only generally applicable way for a function to send a signal to its caller that the caller cannot ignore is to throw an exception. For a variety of reasons, however, the use of exceptions is not always possible or desirable. Examples of code that may want or need to avoid exceptions include:

  • code that is written in a language other than D (for example, C or C++);
  • code written in D that may be called from another language;
  • code that does not want to depend on the D runtime;
  • code that cannot afford the run-time performance overhead of exceptions.

Since D is intended to be a systems language suitable for writing low-level, high-performance code with seamless C and C++ interoperability, its feature set should support reliable error handling in all of these use-cases.

@mustuse helps D achieve this goal by enabling reliable error handling both for functions that use error codes and for functions that use algebraic "result" types to signal failure to their callers.

Alternatives

One possible alternative to exceptions, proposed by Vladimir Panteleev, is for a function to return an error code wrapped in a struct that asserts (or throws) in its destructor at run time if it has not been used (where "using" means calling a method to retrieve the wrapped value).

While this addresses some of the use-cases above, it has one major shortcoming compared to @mustuse: it reports ignored errors at run time rather than compile time.

Another alternative is for a function to return error information via an out parameter. Since a call to the function will not compile with a missing argument, the calling code is forced at compile time to visibly acknowledge the possibility of an error.

Unfortunately, using out parameters for error handling is not a generally applicable solution because the programmer is not always free to change a function's argument list to include an out parameter. Reasons for this include:

  • the function's argument list is part of an established public API, and changing it would break other code;
  • the function is used as a callback by another function that requires it to accept a specific list of arguments;
  • the function is an operator overload.

By contrast, @mustuse requires only the freedom to change a function's return type, and this change can in many cases be made backwards-compatible by using alias this to allow implicit conversion of the new return type to the old one.

Functions without specified side effects

Some functions have side effects but are nevertheless unlikely to be called for those side effects alone. Examples of such functions include:

  • functions that acquire resources, such as malloc and mmap;
  • functions that generate random numbers, such as rand and uniform;
  • generic functions that may or may not cause side effects depending on their arguments, such as filter and map.

What these functions have in common is that their side effects, if any, are considered implementation details, rather than being part of their documented behavior. As a result, calling code cannot rely on them to cause any specific side effects without risking breakage if and when those implementation details change.

Though ignoring the return values of these functions is unlikely to result in disaster, it is still a probable programming mistake which @mustuse could help guard against.

This DIP does not recommend changing the return types of any existing functions in Phobos or the D runtime to @mustuse types, since doing so would constitute a breaking API change. However, authors of new code would still benefit from having @mustuse in the language and existing projects (including Phobos and the D runtime) could adopt @mustuse on a case-by-case basis if the benefit were judged to be worth the potential for breakage.

Prior Work

In D

The D compiler already warns about discarding the value of an expression if it has no side effects, including the case where the expression is a call to a strongly pure and nothrow function. An attribute that would allow the programmer to extend this warning by marking specific functions or types as non-discardable has been proposed several times on the D issue tracker and forums; see the Reference section below for details.

In Other Languages

Cross-Language Comparison Table

Language Attribute Applies to Diagnostic
C++17 [[nodiscard]] Types and functions Warning
Rust #[must_use] Types and functions Warning
C (GCC, Clang) warn_unused_result Functions Warning
D (DIP 1038) @mustUse Types Error

Description

Formal Specification

@mustUse is a compiler-recognized user-defined attribute declared in the D runtime module core.attribute. It takes no arguments.

An expression is considered to be discarded if and only if either of the following is true:

  • it is the top-level Expression in an ExpressionStatement, or
  • it is the AssignExpression on the left-hand side of the comma in a CommaExpression.

It is a compile-time error to discard an expression if all of the following are true:

  • it is not an assignment expression, an increment expression, or a decrement expression; and
  • its type is a struct or union type whose declaration is annotated with @mustUse.

An assignment expression is either a simple assignment expression or an assignment operator expression.

An increment expression is a UnaryExpression or PostfixExpression whose operator is ++.

A decrement expression is a UnaryExpression or PostfixExpression whose operator is --.

It is a compile-time error to annotate any function declaration or aggregate declaration other than a struct or union declaration with the @mustUse attribute. The purpose of this rule is to reserve such usage for possible future expansion.

Diagnostics

An error resulting from @mustUse can be suppressed by prepending cast(void) to the offending expression, since void is not a struct or union type annotated with @mustUse. The error message for discarding an expression of a @mustUse type should suggest using cast(void) if the programmer intended to discard the expression.

Design Goals and Possible Alternatives

The design for @mustUse described above was chosen to achieve the best possible balance among the following goals:

  1. Simplicity of specification. A language feature that is difficult to specify precisely is likely to also be difficult to learn and difficult to use correctly.

  2. Simplicity of implementation. A language feature that has a complex implementation is likely to suffer from implementation bugs. A complex implementation also increases the burden on compiler maintainers, which makes future improvements to both the language and its compilers more difficult to achieve.

  3. Rigor. A language feature that provides strong guarantees that programmers can rely on is more useful that one that provides weak guarantees or permits exceptions and special cases. Rigorous language features compose more easily than non-rigorous ones.

A few possible alternative designs, along with the reasons for their rejection, are discussed below.

@mustUse as a function attribute

As described above, @mustUse is restricted to use with user-defined struct and union types, and cannot easily be applied to functions that return other kinds of types. One way to address this shortcoming would be to allow @mustUse to be used as a function attribute. For example:

// Hypothetical usage
@mustUse int dontIgnoreMe() { return 42; }

void main()
{
    // error: cannot discard return value of @mustUse function `dontIgnoreMe`
    dontIgnoreMe();
}

The main challenge for this design is deciding how to deal with statements that discard the return value of a @mustUse function indirectly. For example:

a ? b : dontIgnoreMe();
(writeln("side effect"), dontIgnoreMe());
(() => dontIgnoreMe())();

There are two possibilities:

  1. Error. This requires all discarded expressions to be searched, recursively, for calls to @mustUse functions in positions where their return values might be discarded.
  2. No error. This allows return values of @mustUse functions to be accidentally discarded in some cases, without an explicit cast or type conversion.

Option (1) sacrificies specification simplicity and implementation simplicity to maintain rigor; option (2) sacrifices rigor to maintain simplicity.

Rather than make either sacrifice, this DIP proposes a design that allows both rigor and simplicity to be maintained, and reserves the possibility for a future DIP to allow @mustUse as a function attribute.

@mustUse as a class attribute

As described above, @mustUse cannot be applied to class or interface types. Allowing this usage would make @mustUse more widely applicable.

The main challenge for this design is deciding how to deal with @mustUse subclasses. For example:

class Parent {}
// Hypothetical usage
@mustUse class Child : Parent {}

Parent fun() { return new Child(); }

void main()
{
    fun();
}

Again, there are two possibilities:

  1. Error. This requires that if a child class is annotated with @mustUse, its parent class must also be annotated with @mustUse.
  2. No error. This allows values of @mustUse class types to be accidentally discarded in some cases, without an explicit cast or type conversion.

Because every D class inherits from Object and Object is not annotated with @mustUse, choosing option (1) would mean restricting @mustUse to interface types and non-D class types (i.e., extern(C++), extern(Objective-C), and COM classes). It is uncertain whether such a restricted version of @mustUse would be worth the complexity cost of specifying and implementing it.

Conversely, option (2) is simpler to specify and implement, but the guarantee it provides is so weak as to make it nearly useless.

Since both options are unattractive, this DIP chooses neither, but reserves the possibility for a future DIP to allow @mustUse as an attribute for classes and interfaces.

Breaking Changes and Deprecations

No breaking changes or deprecations are anticipated.

Reference

Copyright & License

Copyright (c) 2020 - 2022 by the D Language Foundation

Licensed under Creative Commons Zero 1.0

Reviews

Community Review Round 1

Reviewed Version

Discussion

Feedback

The following points were raised in the feedback thread:

  • How will the cast semantics interact with @safe? The DIP author responded that cast(void) is always @safe.
  • @nodiscard should not apply to void functions. The DIP author agreed but noted that it's not transitive and can never be inferred, so the solution is simply for programmers not to apply it to void functions. The equivalent features in C++ and Rust do not make this exception.
  • Regarding the "Error handling without exceptions" subsection of the Rationale:
    • It's possible to build a D interface that throws exceptions when binding to other languages
    • The commenter is aware of at least one package marked "optimized for fast execution" that throws
    • The numbers are inaccurate. The DIP author is aware of this, but the effort required for complete accuracy is prohibitive, and he believes the proposal is stronger even with inaccurate numbers than without.
  • The DIP doesn't mention constructors. The DIP author replied that the rules cover this implicitly since constructors are functions.
  • The DIP should mention if @nodiscard applies when the attribute is attached to a type and a constructor is called for that type. The DIP author replied that a call to a type's constructor is an expression of that type and, therefore, when @nodiscard is applied to the type, the expression is non-discardable.
  • The DIP does not cover template functions with auto-inferred types, the return type of which can be void or not depending upon the template arguments. The DIP author replied he could not imagine a need to mark such a function as @nodiscard.

Final Review

Reviewed Version

Discussion

Feedback

There were only two actionable items of feedback in this round. One was about an ambiguity in the text. The other:

Given a function @nodiscard int foo called in, e.g., a ternary operation a ? b : foo(), there is no error raised about the return value of foo being ignored. The reviewer sees this as a critical shortcoming that should be addressed. The DIP author responded that this behavior is identical to that of GCC, C++17, and Rust. He explained that such error cases could be detected if the annotation were a type qualifier, e.g., @nodiscard(int) foo, but that can lead to undesirable errors. He suggested that syntax-level checks could be removed from the proposal if they are deemed inadequate.

Formal Assessment

The language maintainers accepted this DIP with a request for changes:

  • rename @noDiscard, as they want to avoid adding addional negative attributes to the language.
  • address issues that arise from the feature's interaction with inheritance when applied to classes.
  • develop rules for handling covariance and contravariance when applied to functions.

The DIP author addressed these requests by renaming the attribute to @mustuse and allowing it only on structs and unions. His rationale for the latter is described in the section, Design Goals and Possible Alternatives.

The maintainers approved the author's changes and accepted the revised version of the DIP.