Skip to content
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

Make Error and ErrorOr copyable and assignable #1152

Closed
wants to merge 862 commits into from
Closed

Make Error and ErrorOr copyable and assignable #1152

wants to merge 862 commits into from

Conversation

geoffromer
Copy link
Contributor

No description provided.

jonmeow and others added 30 commits September 23, 2021 09:16
With #842 there's not as much reason for the BUILD anymore, maybe move the template.md file and get rid of it?
Allow `auto` as a return type for functions. Only support functions with one `return` statement for now (open question).

This explicitly suggests removing the executable semantics `fn name(args) => expression` syntax, and is motivated by reconciling executable semantics with approved Carbon state. [example](https://github.com/carbon-language/carbon-lang/blob/7b706ad7446fe5ac8dc9c092155c8df371d49a71/executable_semantics/testdata/fun_named_params.carbon) This aspect is a decision that may be affected by lambda syntax, but we might also choose to keep lambda syntax and function syntax separate -- I don't think there's enough benefit to providing the alternate function syntax right now.

Co-authored-by: Geoff Romer <[email protected]>
Co-authored-by: Chandler Carruth <[email protected]>
This proposal introduces the operators `==`, `!=`, `<`, `<=`, `>`, and
`>=` to Carbon.

Co-authored-by: Jon Meow <[email protected]>
Co-authored-by: Geoff Romer <[email protected]>
Co-authored-by: josh11b <[email protected]>
This proposal provides semantics for numeric literals.

* Numeric literals have a type derived from their value, and can be converted to any type that can represent that value.

* Simple operations such as arithmetic that involve only literals also produce values of literal types.

* Literals implicitly convert to types that can represent them.

* The Carbon prelude provides:
    * An arbitrary-precision integer type `BigInt`.
    * A rational number type `Rational(T:! Type)` with constraints on `T` not yet determined.
    * A family of integer literal types, `IntLiteral(N:! BigInt)`.
    * A family of real literal types, `RealLiteral(N:! Rational(BigInt))`.

Co-authored-by: Chandler Carruth <[email protected]>
Co-authored-by: Jon Meow <[email protected]>
Revives BisonWrap because this seems a reasonable use of it (avoiding the need to have an std::optional or pointer for Alternative, both of which I thought could be unclear about the intent).
The code is pretty intertwined: having the AST be truly mutable means (to me) changing parser.ypp to return non-const values, but then the way things are passed around between objects should be non-const (particularly an issue with lists), which then creates issues with construction of lists in the TypeChecker, which then TypeChecker needs to mostly be non-const.

Due to the difficulties in breaking this apart, whereas I'd previously considering refactoring accessor naming in the same PR, I've largely avoided doing so. The intent is then that this PR focuses mainly on const -> non-const AST behavior.

call_main moves out of interpreter.cpp so that interpreter.cpp can receive a fully const AST.
This implements #826, I think covering everything important there.

Regarding ReturnTypeContext, I broke that out because it started feeling like a significant number of args to be passing around, and I think this makes the association inside type checking clearer.

Co-authored-by: Geoff Romer <[email protected]>
- Drop unnecessary special-casing of 0-tuples.
- Drop unused constructor.
This proposed style change allows C++ classes in the Carbon project to provide methods that are named like variables, so long as they behave like _properties_ of the class. It also requires data member names to have a trailing `_`.

Co-authored-by: Chandler Carruth <[email protected]>
Updates style of the Match class at the same time.
This does a mass rename of:

-   `SourceLoc()` -> `source_loc()` for property naming
    - `loc` -> `source_loc_` for underscore+consistency
    - Generally changing function args to `source_loc` for consistency
-   `Tag()` -> `kind()` for property naming and `Kind` parity
    - `tag` -> `kind_` for underscore

Also renames `Pos` and `Results` on `Action`. These are a bit of an exception in that most base classes only have `Tag` and maybe `SourceLoc`, whereas `Action` has a little more. I felt okay having `source_loc()` and `kind()` on the base class where children do `Exp()` and the like, but it felt weird to me to mix it on the same class.

The reason for doing this cross-class in one PR is so that I can do it efficiently with a global replace in the codebase, rather than e.g. changing `Expression` but having to read through compiler errors to determine where it's calling `Expression`'s `Tag` versus a different `Tag`. The end result should be equivalent.
This should get us actual debugging capabilities.

Also, adopt the better bazel formulation that I forgot to add from
geoffromer's original change.
Previously, the program exit was triggered by the destructor.
Unfortunately, C++ doesn't make it precisely clear where the destructor
is run, and Clang doesn't generate reliable debug information for that
to give good backtraces. Among other things, when combining separate
cleanup regions in Clang there may be no single canonical location.

Instead, move the ExitingStream system to use an explicit
low-precedence operator overload to flush the output and exit. This
ensures the stream and other actions are completed first but then
immediately exits the program in a way that has a definitive source
location and produces reliable backtraces.

I've tried to add comments and helpers to make this as clear as possible
given that it is a subtle and surprising issue.

Co-authored-by: Geoff Romer <[email protected]>
- Replace obsolete references to the arbiters, and clarify that the appeals process will be available once the leads are separate from the conduct team.
- Document that leads, like conduct team members, will be excluded from discussions/decisions that concern them.
This list was extracted from currently-approved proposals and
corresponding design documents. This is intended to be a summary of the
status quo, not a change.
* Make tuple/struct fields mutable.
* Avoid ExpectType when we know it will fail.
The LLVM-13 release is now on Homebrew, so switch to it. This is
important because our CI only installs the latest version currently.

If you hit issues after this, make sure to update your Homebrew install
to get the latest Clang release. You can always directly set `CC` in
your environment to use a specific `clang` compiler.
Prior to this change, `__await` would make a deep copy of the continuation stack, but shallow-copy the individual stack frames within it. As a result, continuations appeared to have shallow semantics so long as the continuation stack had only a single frame.

This change also removes an obsolete test from the brief period when we intended continuations to have deep-copy semantics, which has been passing basically by accident.
I should emphasize that I am **completely cheating** here. This PR does not add support for actually _performing_  implicit conversions at run time, because the AST doesn't yet contain the necessary type information. At run time, code like `var p: Point = {.x = 1, .y = 2};` directly initializes the name `p` with the _struct_ value `{.x = 1, .y = 2}`; no object of type `Point` is actually created. I'm only getting away with this because we don't yet have any tests that can tell the difference.

Co-authored-by: Jon Meow <[email protected]>
geoffromer and others added 23 commits March 10, 2022 16:46
* Modify parser and AST nodes to include let statement

* Implement let variables

* Remove redundant code in fail_match_choice test

* Add comment for has_value_category()

* Apply suggestions from code review

Co-authored-by: Jon Meow <[email protected]>

* clang-format changes

* Update comments for new BindingPattern methods

* Implement nested vars in patterns

* Apply suggestions from @geoffromer's code review

Co-authored-by: Geoff Romer <[email protected]>

* Implement changes from code review.

* Remove maybe_var_pattern grammar rule

Co-authored-by: Jon Meow <[email protected]>
Co-authored-by: Geoff Romer <[email protected]>
Note the use of an enum allows for the operator to be defined for formatting. This reduces the custom formatting required, which is a simplification carrying over to error handling changes I'm working on too.

Also, I kind of feel the code's now clearer about what's supported; I'm dropping CHECKs that seemed redundant with the enum.
…1138)

This is following #1119 .

* Updates terminology.md, overview.md, details.md
This proposal addresses the question of whether we should perform a fully top-down compilation (like in C), a mostly top-down compilation (like in C++), or whether we should allow information from later in the same source file to be used in earlier program constructs (like in Rust, Swift, Java, C#, Haskell, and so on).

The proposed direction is:

-   Entities declared later in the same source file cannot be used earlier; top-down semantics apply everywhere.
    -    As an exception, class member function bodies are parsed as if they appeared after the class.
-   Forward declarations can be used to separate interface from implementation and to allow entities to be used before they are defined.
-   The behavior of the program is nonetheless required to be the same as if we had a globally-consistent rule: it's always a hard error to depend on any information that is not known or that is provided later.
Roadmap updates for 2022. Key goals:

* Reach the point where the first draft of the core language design is complete, with some test programs and an approximate implementation in executable semantics.
* Go public, and improve public participation.

Co-authored-by: Jon Meow <[email protected]>
Provide an initial rough structure for the specification so we can add things
as they are decided.

Split the specification into a language and a library section. In the language
section, use one file per broad area of functionality. Divide the language up
based on the intended layering of the language design.
This comes from #1092 where I wrote bad code and I felt it should have been caught more clearly.
Revised landing page, including updated README.

Co-authored-by: josh11b <[email protected]>
Co-authored-by: Chandler Carruth <[email protected]>
Co-authored-by: Jon Meow <[email protected]>
Co-authored-by: Geoff Romer <[email protected]>
Co-authored-by: Chandler Carruth <[email protected]>
Separating out existing fixes because of #1148 which turns on more.
…like invalid syntax (#1120)

* Replaced std::exit() with return llvm::Expected/llvm::Error<T> for expected errors like invalid syntax.

* Use llvm::formatv() for formatting lexer error messages.
x

* Addresed merge errors.

* Fixed impl scope.

* Made ErrorBuilder::operator<< nodiscard, to catch code forgetting 'return' in 'return FATAL_COMPILATION_ERROR()'.

* FatalComplationError() -> ParseAndLexContext::RecordLexerError().
Other usages of ERROR_TOKEN in lexer.lpp were actually supposed to be END_OF_FILE.

* Update executable_semantics/syntax/parse_and_lex_context.h

Co-authored-by: Jon Meow <[email protected]>

* Code review fixes.

* Update executable_semantics/syntax/parser.ypp

Co-authored-by: Jon Meow <[email protected]>

* More code review fixes.

* Update executable_semantics/interpreter/type_checker.h

Co-authored-by: Jon Meow <[email protected]>

* Yet more code review fixes...

* Update executable_semantics/syntax/lexer.lpp

Co-authored-by: Jon Meow <[email protected]>

* code review comments

* Update executable_semantics/interpreter/interpreter.cpp

Co-authored-by: Geoff Romer <[email protected]>

* Apply suggestions from code review

Co-authored-by: Jon Meow <[email protected]>

* Update executable_semantics/syntax/lexer.lpp

Co-authored-by: Jon Meow <[email protected]>

* code review

* code review

* Apply suggestions from code review

Co-authored-by: Jon Meow <[email protected]>

* formatted code

* review comments

* Switched to the new ErrorOr<V> error implementation

* code review comments

* fixed comment

* restored ostream.h as #976 makes the change unnecesary

* review comments

Co-authored-by: Jon Meow <[email protected]>
Co-authored-by: Geoff Romer <[email protected]>
Looks like this might be related to prettier/prettier#6112 , but the observed behavior is more broken than that bug indicates.
@geoffromer geoffromer requested a review from a team as a code owner March 24, 2022 20:16
@@ -26,7 +26,10 @@ class [[nodiscard]] Error {
CHECK(!message_.empty()) << "Errors must have a message.";
}

Error(Error&& other) noexcept : message_(std::move(other.message_)) {}
Error(const Error&) = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, what's the motivation for the change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The style guide encourages making types copyable when copying has a clear meaning, and discourages using const&/&& overload pairs. This PR aligns the code with both principles.

Note that llvm::Error seems to be move-only as part of its mechanism for preventing code from ignoring the error, but we've solved that problem with [[nodiscard]] instead.

Copy link
Contributor

@jonmeow jonmeow Mar 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the main rationale is about style, I'll note:

"A type should not be copyable/movable if the meaning of copying/moving is unclear to a casual user, or if it incurs unexpected costs."

Just to note, my original intent had been to minimize accidental copy performance overhead (as copies may be expensive, particularly for ErrorOr). I believe in the toolchain we'd want to be more cautious about copies, and not providing a copy constructor is one way to avoid incurring related costs.

This may be more appropriate for executable_semantics, but maybe then the class should be moved correspondingly in order to decrease the likelihood of reuse elsewhere in the toolchain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Accidental" is importantly different from "unexpected" here. That passage of the style guide is saying you should disable copying if it costs meaningfully more than typical users would expect it to cost. It's not saying that you should disable copying if a user can incur unexpected costs by copying when they didn't mean to. If it was, that exception would swallow the rest of the rule, because any type holding a nontrivial amount of data can "incur unexpected costs" by that definition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I disagree -- I don't feel confident about when a copy will occur, and when a move will occur. To say that when I write a copy when I intend for a move is an "accident" and not "unexpected" ignores what I expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you arguing to completely remove the checks that errors are handled? Basically, removing nodiscard and the bool?

No, I prefer the status quo on both points, but the status quo is not what you think it is: there's no bool. Early revisions of #1137 had it, but it was removed during review. Neither Error nor ErrorOr has any run-time tracking of whether the error has been consumed. They do have some compile-time enforcement through nodiscard, but that's at best a weak approximation of linear typing, and certainly doesn't ensure the error is consumed. For example, it doesn't prevent bugs like this:

ErrorOr<Success> err = MightFail();
// ...
return Success();

nodiscard merely ensures that the author (and sufficiently attentive readers) are aware that MightFail() returns something. In other words, it ensures that bugs like the above are at least visible in the calling function.

Especially for ErrorOr, essentially all of the comparable designs I'm aware of use both nodiscard and a bool to track that the error is consumed.

absl::StatusOr doesn't have such a bool, and neither does the proposed std::expected. llvm::Error and llvm::ErrorOr do, but it comes at a substantial ergonomic cost: there's no way to read the error message without consuming the object. That means, for example, that they interoperate poorly with GoogleTest (as we discovered as soon as we started trying to use them), because GoogleTest's design pervasively assumes that you can observe the salient state of a value with const operations.

Conversely, I've never heard complaints about the lack of error-consumed tracking in either absl::StatusOr (which I used to co-own) or std::expected. It doesn't even seem to come up in the design rationale for std::expected, which addresses the feedback they've received pretty exhaustively. So I'm much more inclined to follow the examples of StatusOr and expected here, and both of those types are copyable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the combination of [[nodiscard]] on the type (not just on a function), but zero dynamic checking... odd.

I guess this really gets to [[nodiscard]] on a type not meaning what I think it means. It doesn't mean an obligation to the holder at all, it just adds an obligation to the caller of a function that returns the type to do something with the returned value. Those values have no subsequent obligation at all. From that sense, I think I'm convinced that being copyable makes a lot of sense. But this hinges on [[nodiscard]] not creating on obligation to holder (to address @jonmeow point above) and thus copying it not creating more obligations (to address @zygoloid's point before that).

I'm honestly surprised that this pattern has worked well for absl::StatusOr and std::expected but 🤷🏻 - I guess I get to live with being surprised.

Curious whether this makes @zygoloid and @jonmeow happy or not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, what you're saying is that we can't completely rely on [[nodiscard]] because it doesn't handle every situation. It only errors when the return type is immediately discarded, for example:

MightFail();
return Success();

And as a consequence of not handling any case, there's no obligation for the type per se. Is that correct?

Note though, not every function is going to be named MightFail(), it may be something like AddEntity and not obviously have a failure mode. [[nodiscard]] means there will be something at the call site for a human reviewer to see (or author to hit a compile error and fix). Thus, even as an imperfect obligation, it still shifts more of the review burden to a compiler check.

I think that either way you go, not everybody's going to be happy. But if the decision is to not rely on these imperfect compiler checks, and especially if it hinges on [[nodiscard]] being imperfect, would that imply we should remove [[nodiscard]] everywhere in Carbon and simply rely on review to handle it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not remove [[nodiscard]] ;) For example, in the std::exit -> return-error conversion PR a nodiscard on ErrorBuilder flagged two places where I forgot to add return to the COMPILATION_ERROR() macro, which would have been very hard to spot during manual review process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, what you're saying is that we can't completely rely on [[nodiscard]] because it doesn't handle every situation. It only errors when the return type is immediately discarded, for example:

MightFail();
return Success();

And as a consequence of not handling any case, there's no obligation for the type per se. Is that correct?

I'm saying that we can't rely on it to enforce a linear typing/"obligation" model, because that's not what it's designed to do. I think we can rely on it to ensure that potential discarded-error bugs are locally visible.

Note though, not every function is going to be named MightFail(), it may be something like AddEntity and not obviously have a failure mode. [[nodiscard]] means there will be something at the call site for a human reviewer to see (or author to hit a compile error and fix). Thus, even as an imperfect obligation, it still shifts more of the review burden to a compiler check.

I think that either way you go, not everybody's going to be happy. But if the decision is to not rely on these imperfect compiler checks, and especially if it hinges on [[nodiscard]] being imperfect, would that imply we should remove [[nodiscard]] everywhere in Carbon and simply rely on review to handle it?

On the contrary, if we're going to "rely on review" to detect discarded-error bugs (and I think we have to, at least as a backstop), that seems like a strong argument for keeping nodiscard. As you noted in the previous paragraph, one of the main things nodiscard is good at is ensuring that there's something at the callsite for the reviewer to see.

common/error.h Outdated
CHECK(!ok());
return std::get<Error>(val_);
}
auto error() && -> Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does removing this mean the code like return Error(std::move(result_or_error).error()) won't be able to use Error's move constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but since Error is copyable, that code will still work, it just might be less efficient. I wouldn't be opposed to adding a non-const overload of error(), which would let people move out of it, but I'm not sure it's necessary.

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants