Pre-RFC: Adjust default object bounds

Your definition of breakage is not very useful. It’s all about cost (see my analysis above, too). Yes, we want to avoid people no longer being able to compile code. But we also want to avoid people flocking to google to find out how to do it.

We want to minimize both costs, and if we cannot have both, pick the solution that costs the least. All analysis points to the cost of the former being quite low, while we get reports of people tripping over the current setup, which suggests the latter costs us more dearly.

Now if you want to talk about the cost for Rust’s image, an argument can be made both for and against this change. But I have seen many people doing stupid things out of fear of looking stupid, so I think this point is pretty moot.

Dismissing social cost as unwarranted fear is unwise, I think. Breaking changes in Rust has been a huge factor in keeping people away from the language before 1.0. A breaking change---even in name only like this one seems to be---makes it harder to convince others to try Rust that are concerned about its stability.

To be clear, I don't have a terribly strong opinion on this change. But I also don't want to see important costs dismissed.

The former definition is something we, as a community, have decided against. For example, the presence of glob imports implies that any addition to std is a breaking change. Because of subtleties like that, we have two RFCs that outline what we consider to be breaking changes: rfcs/text/1122-language-semver.md at master · rust-lang/rfcs · GitHub and rfcs/text/1105-api-evolution.md at master · rust-lang/rfcs · GitHub.

1 Like

You're of course right. I just want us to not fall into the trap of not doing something because it may be unpopular (or conversely, doing something because we think it is popular).

Thinking some more about the technical problem at hand, could rustc perhaps infer a kind of superposition of 'a and 'static and per default decide for 'a unless this would result in errors? In the latter case, a suitable warning could be issued. This would

  • completely remove any breakage that would result from choosing either option
  • allow people to gradually update their code if it depends on 'static inference
  • show people that we take backcompat very seriously
  • greatly complicate the implementation, if it is possible at all.

This is because the cost of people tripping over the proposed setup has not been analysed. And it might be difficult to analyse it too, I'm just saying we shouldn't assume that the cost is zero just because it has not been analysed.

1 Like

Argh, I meant to post this yesterday, but I did indeed get distracted.

I think I am basically arguing for not changing the language in backwards incompatible ways, except for two cases:

  1. major version boundaries (and then, with restraint)
  2. when we believe the impact to be negligible

In particular, I think all the downsides that I outlined (tutorial rot etc) are mitigated if the impact has negligible impact. Basically, if nobody has written code using this particular thing, it's probably not the subject of many tutorials either!

Now where I think reasonable people can easily differ is on the question of whether we are truly capable of judging that a change has negligible impact -- and in particular one might argue that my criteria are not sufficient. I have some ideas for improving our ability to assess this (e.g., some kind of opt-in program for private repos, with an NDA if necessary) but I'd love to hear others.

In any case, in the end, as I wrote, I don't expect this scenario to arise often. I think we'll be able to avoid it by more aggressive use of beta testing and so forth, particularly for major features. I imagine that whenever it does, we'll want to reassess both the importance of the change and the criteria we use to estimate its impact.

Yes, there is definitely risk if we appear to be making capricious changes. I think that this change is quite well-motivated in terms of improving the language, and I think we have adequate justification to say that overall we believed its impact to be very, very low. (And clearly, we can show that we considered the impact.) Still, there is risk that this estimate is misleading.

I like this summary, yes. And I have been investigating ways to improve the diagnostics -- I'll try to put more time into that today.

I wanted to note an idea here that for such “course corrections” (as @nikomatsakis aptly dubbed them), it makes a lot of sense to use an opt-out rather than an opt-in. The proposed opt-out would be a crate-level attribute like #![legacy(default_object_bounds)], which you can use to recover the old behavior.

An opt out for such cases has several nice properties:

  • Most importantly, it means that the breakage is truly minimal in all cases: at worst, you have to add a single attribute to the crate to fix it. We retain the guarantee of hassle-free upgrades.

  • But, since we believe there to be very little code that will actually break as a result of this change, we likewise would expect very few crates to have to apply the attribute.

  • Once we add support for legacy in the compiler, it becomes possible to straddle new and old versions by using the attribute. Suppose we make a change in 1.4 with a legacy opt out. If you use the opt-out, you should still be able to compile your code on 1.3 – it won’t recognize the opt out, but it doesn’t need to, because it already has the old behavior!

  • Unlike opt-in flags, these legacy flags would not tend to accumulate over time for code tracking stable Rust. On the contrary, there is good incentive to remove use of the flag (similar to addressing deprecation warnings).

4 Likes

I answered to @aturon on reddit but realized that Discuss was the official channel, so allow me to repeat myself.

I believe that people are okay for breaking changes, as long as they are effortless. Case in point, the gofix tool was praised for the ability to update from Go 1.x to Go 1.y without actually having to manually edit the code.

Therefore, it seems that minor breaking changes would be much more acceptable if the only thing to do was using a “rustfix” tool (supplied with the versions x and y (**)).

And I posit that should the rustc team be willing to add a legacy attribute for each such minor breaking change (*), then the “rustfix” tool would be trivial enough: it would just find the “main” files (lib.rs, etc…) and add the appropriate collection of #![legacy(...)] crate level attributes there.

There. No dreadful upgrade path. It just works.

(*) I assume here that the distinction between minor/major is that major changes would cause such a tremor in rustc/libraries that it would just be way too costly to keep both versions in parallel OR that keeping the older version would just be too dangerous (security-wise).

(**) Or make the tool argument-less by (1) shipping the tool with rustc (so it knows the current version) and (2) assume that backward compatibility should be maintained with 1.0 (by default).

A ‘rustfix’ tool would be nice, but given the resources and focus of the core team, it would have to be mostly in the hands of the community, like is the case with ‘rustfmt’, which would probably eventually happen anyway, but will take some time, meaning that this particular situation would have to be resolved by consensus.

Given how early in the lifetime of the “stable” Rust we are and that this change seems to fit into the exceptions in the “Promise of Stability” blog post, I’ll vote for making the change now, rather than having to live with the consequences untill a hypothetical 2.0, which could be years away.

I tend to like @aturon’s solution the most.

I speculate that largely unmaintained codebases will draw the strongest value out of having hassle-free rather than hassle-epsilon upgrades. The current design misses two useful properties for that scenario: there is no proposed mechanism for the maintainer abandoning a code base to specify “opt-out of as many future edge case fix-ups as possible”, and an inheritor/user of his program (who hypothetically isn’t familiar with Rust) would have to learn about opt-out flags and find the correct one if it failed to compile on a new rustc. The first concern would be nicely addressed if we allowed opting out by version, e.g. #![legacy("1.0.*")].

A separate issue with the specific change is that library authors who wish to preserve 1.0 compatibility will have to be aware of the change and accommodate it, either by using the legacy flag or manually avoiding defaulting. This could mitigate adoption of the new semantics. Also, library consumers running 1.0 might be significantly more confused by a type related error message than if their dependency had included references to new APIs.

As I’ve already noted on the RFC itself, a #[legacy(...)] flag has the severe downside of requiring all dependencies to update themselves, which we should not rely on. People may not be able to change their dependencies for legal or other reasons and requiring a cargo/rustc flag per legacy dependency gets very finicky – I would not want to have to tell this story to devops.

This was one of the main motivators for going with target versions with my RFC PR, because they can never go stale. The good thing now is, we can add them in a backwards-compatible fashion, and it won’t even require us to create a special tool for it.

Thinking some more about this, we could have both target version + feature-opt-in.

It should be feasible to write a simple script -- "restore_old_box_behavior.pl", say -- that replaces any instances of &'x Box<Trait> with &'x Box<Trait + 'x> throughout a codebase. In practice, that's probably better than &legacy[], because it takes the same amount of time, but it will work until Rust 2.0 without any versioning guarantees. The other big downside of versioning is that over time it significantly increases the surface area for an alternate Rust implementation to support, when ideally we should start to see such implementations pretty soon. It's also possible where this newly-introduced error occurs, even in an old, unmaintained codebase, for rustc to suggest the appropriate change in a compiler error message.

The other advantage of using a script instead of introducing #legacy[] is that (because of the extra work involved) it continues to send the message that breaking changes will be extremely few and far between. If #legacy[] is part of rust, you're telling people it's unstable, one way or the other.

I'm cautiously in favor of this change because:

  • all of the old behavior is still fully accessible by merely changing type annotations

  • such a script will always fix any code that depends on the old behavior

  • as long as said code does not link to code that depends on the new behavior

In this case, the caveat is hilariously unlikely; even so, only a little more type-annotation-juggling will probably fix it. You could probably refer complainants to this part of the 1.0 blog post:

We reserve the right to fix compiler bugs, patch safety holes, and change type inference in ways that may occasionally require new type annotations.

Type aliasing isn't quite type inference, but it's close enough to fool all but the most intransigent type-theorists. A slightly weaker (but still very strong) stability guarantee that this does not violate is:

no semantic changes after 1.0 (except bugfixes)

I’m strongly against investing time writing such a script, because:

  • People may not be able or willing to have a script run over their dependencies. (Imagine telling a middle manager of a financial institution that you have to rely on code that was possibly modified by some “simple script”?)
  • Even if we agree to run it, the script must be run manually, thus requiring additional effort on every build of every new module.
  • I doubt the script will really be that simple. It took me some time to even understand the issue, and there may be subtle corner cases lurking (e.g. code generated by macros) where your script will fail to match the required code (Having written a few lints for rust-clippy, I can attest that this can happen). Hilarity ensues.
  • I disagree that writing such a script will assuage the naysayers or as you say, “send the message that breaking changes will be extremely few and far between” – I think it rather sends the message that as long as the extra steps required to fix it can be automated, however precariously, we are OK with breakage.
  • Anyone remembers python’s 2to3 tool? The similarity is striking.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.