Skip to content

Commit df1efff

Browse files
committed
Pare back the RFC to just the minimum: guidelines on making breaking
changes.
1 parent 23fa07e commit df1efff

File tree

1 file changed

+24
-253
lines changed

1 file changed

+24
-253
lines changed

text/0000-language-semver.md

+24-253
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55

66
# Summary
77

8-
This RFC has two main goals:
9-
10-
- define what precisely constitutes a breaking change for the Rust language itself;
11-
- define a language versioning mechanism that extends the sorts of
12-
changes we can make without causing compilation failures (for
13-
example, adding new keywords).
8+
This RFC has the goal of defining what sorts of breaking changes we
9+
will permit for the Rust language itself, and giving guidelines for
10+
how to go about making such changes.
1411

1512
# Motivation
1613

@@ -29,29 +26,11 @@ disruptive to the ecosystem. Therefore, **the RFC also proposes
2926
specific measures to mitigate the impact of breaking changes**, and
3027
some criteria when those measures might be appropriate.
3128

32-
Furthermore, there are other kinds of changes that we may want to make
33-
which feel like they *ought* to be possible, but which are in fact
34-
breaking changes. The simplest example is adding a new keyword to the
35-
language -- despite being a purely additive change, a new keyword can
36-
of course conflict with existing identifiers. Therefore, **the RFC
37-
proposes a simple annotation that allows crates to designate the
38-
version of the language they were written for**. This effectively
39-
permits some amount of breaking changes by making them "opt-in"
40-
through the version attribute.
41-
42-
However, even though the version attribute can be used to make
43-
breaking changes "opt-in" (and hence not really breaking), this is
44-
still a tool to be used with great caution. Therefore, **the RFC also
45-
proposes guidelines on when it is appropriate to include an "opt-in"
46-
breaking change and when it is not**.
47-
48-
This RFC is focused specifically on the question of what kinds of
49-
changes we can make within a single major version (as well as some
50-
limited mechanisms that lay the groundwork for certain kinds of
51-
anticipated changes). It intentionally does not address the question
52-
of a release schedule for Rust 2.0, nor does it propose any new
53-
features itself. These topics are complex enough to be worth
54-
considering in separate RFCs.
29+
In rare cases, it may be deemed a good idea to make a breaking change
30+
that is not a soundness problem or compiler bug, but rather correcting
31+
a defect in design. Such cases should be rare. But if such a change is
32+
deemed worthwhile, then the guidelines given here can still be used to
33+
mitigate its impact.
5534

5635
# Detailed design
5736

@@ -247,229 +226,38 @@ future as well. The `-Z` flags are of course explicitly unstable, but
247226
some of the `-C`, rustdoc, and linker-specific flags are expected to
248227
evolve over time (see e.g. [#24451]).
249228

250-
### Opt-in changes
251-
252-
For breaking changes that are not related to soundness or language
253-
semantics, but are still deemed desirable, an opt-in strategy can be
254-
used instead. This section describes an attribute for opting in to
255-
newer language updates, and gives guidelines on what kinds of changes
256-
should or should not be introduced in this fashion.
257-
258-
We use the term *"opt-in changes"* to refer to changes that would be
259-
breaking changes, but are not because of the opt-in mechanism.
260-
261-
#### Rust version option
262-
263-
The specific proposal is to introduce a command-line option
264-
`--rust-version=X.Y[.Z]` that instructs the Rust compiler to expect
265-
source code from older versions of Rust. This option could also be
266-
specified in a `Cargo.toml` file in a `rust-version` property. The
267-
version applies to the crate currently being compiled and is called
268-
the crate's "supplied version". Every build of the Rust compiler will
269-
also have a version number built into it reflecting the current
270-
release; if the command-line option is not supplied, the compiler
271-
defaults to this builtin version.
272-
273-
The supplied version is used by the compiler to produce the semantics
274-
of Rust "as it was" during version `X.Y`. RFCs that propose opt-in
275-
changes should discuss how the older behavior can be supported in the
276-
compiler, but this is expected to be straightforward: if supporting
277-
older behavior is hard to do, this may be an indication that the
278-
opt-in change is too complex and should not be accepted.
279-
280-
Note that the supplied version may affect the parser configuration
281-
used when parsing the initial crate, since it can affect the keywords
282-
recognized by the tokenizer and perhaps other minor details in the
283-
syntax. However, because the version is supplied on the command line,
284-
this configuration is known before parsing begins.
285-
286-
#### Defaults and extreme cases
287-
288-
If no version is supplied on the `rustc` command line, `rustc` will
289-
default to the maximal version it recognizes. If the user supplies a
290-
version `X.Y` that is *newer* than the compiler itself, the compiler
291-
should simply issue a warning and proceed as if the user had supplied
292-
the compiler's version (i.e., the newest version the compiler knows
293-
about).
294-
295-
Cargo will always invoke `rustc` with a supplied version. If there is
296-
no version in the `Cargo.toml` file, then `1.0.0` is assumed. (It may
297-
be a good idea to issue a warning in this case as well.)
298-
299-
Whenever a new project is created with `cargo new`, the new
300-
`Cargo.toml` will include the most recent Rust version number by
301-
default. (Since Cargo and rustc are not, at least today, necessarily
302-
released on the same schedule, we'll have to pick some sensible
303-
definition of the "most recent" Rust version number; one option is to
304-
query the `rustc` executable in scope. Another is to synchronize the
305-
release schedules and use the "built-in" notion.)
306-
307-
Note that the defaults for `rustc` and `cargo` differ. `rustc` prefers
308-
the most recent verison of Rust by default, whereas `cargo` prefers
309-
the oldest. The reason is that we expect running `rustc` in a
310-
standalone fashion to be used primarily when experimenting with small
311-
scripts and one-offs, and the user is most likely to want "current
312-
Rust" in that scenario.
313-
314-
#### When opt-in changes are appropriate
315-
316-
Opt-in changes allow us to greatly expand the scope of the kinds of
317-
additions we can make without breaking existing code, but they are not
318-
applicable in all situations. A good rule of thumb is that an opt-in
319-
change is only appropriate if the exact effect of the older code can
320-
be easily recreated in the newer system with only surface changes to
321-
the syntax.
322-
323-
Another view is that opt-in changes are appropriate if those changes
324-
do not affect the "abstract AST" of your Rust program. In other words,
325-
existing Rust syntax is just a serialization of a more idealized view
326-
of the syntax, in which there are no conflicts between keywords and
327-
identifiers, syntactic sugar is expanded, and so forth. Opt-in changes
328-
might affect the translation into this abstract AST, but should not
329-
affect the semantics of the AST itself at a deeper level. This concept
330-
of an idealized AST is analagous to the "elaborated syntax" described
331-
in [RFC 1105], except that it is at a conceptual level.
332-
333-
So, for example, the conflict between new keywords and existing
334-
identifiers can (generally) be trivially worked around by renaming
335-
identifiers, though the question of public identifiers is an
336-
interesting one (contextual keywords may suffice, or else perhaps some
337-
kind of escaping syntax -- we defer this question here for a later
338-
RFC).
339-
340-
In the previous section on breaking changes, we identified various
341-
criteria that can be used to decide how to approach a breaking change
342-
(i.e., how far to go in attempting to mitigate the fallout). For the
343-
most part, those same criteria also apply when deciding whether to
344-
accept an "opt-in" change:
345-
346-
- How many crates on `crates.io` would break if they "opted-in" to the
347-
change, and would opting in require extensive changes?
348-
- Does the change silently change the result of running the program,
349-
or simply cause additional compilation failures?
350-
- Opt-in changes that silently change the result of running the
351-
program are particularly unlikely to be accepted.
352-
- What changes are needed to get code compiling again? Are those
353-
changes obvious from the error message?
354-
355-
Another important criterion is the implementation complexity. In
356-
particular, how easy will it be to maintain both the older behavior
357-
and the newer behavior? It is important to consider not just the
358-
complexity today, but possible complexity in the future as the
359-
compiler changes.
360-
361229
# Drawbacks
362230

363-
**Allowing unsafe code to continue compiling -- even with warnings --
364-
raises the probability that people experiences crashes and other
365-
undesirable effects while using Rust.** However, in practice, most
366-
unsafety hazards are more theoretical than practical: consider the
367-
problem with the `thread::scoped` API. To actually create a data-race,
368-
one had to place the guard into an `Rc` cycle, which would be quite
369-
unusual. Therefore, a compromise path that warns about bad content but
370-
provides an option for gradual migration seems preferable.
371-
372-
**Deprecation implies that a maintenance burden.** For library APIs,
373-
this is relatively simple, but for type-system changes it can be quite
374-
onerous. We may want to consider a policy for dropping older,
375-
deprecated type-system rules after some time, as discussed in the
376-
section on *unresolved questions*.
231+
The primary drawback is that making breaking changes are disruptive,
232+
even when done with the best of intentions. The alternatives list some
233+
ways that we could avoid breaking changes altogether, and the
234+
downsides of each.
377235

378236
## Notes on phasing
379237

380238
# Alternatives
381239

382-
**Use an attribute rather than command-line option.** Earlier versions
383-
of this RFC used a `#[rust_version]` attribute to specify the Rust
384-
version rather than a command-line parameter. This was changed to use
385-
a command-line parameter because it (a) exposes the version int he
386-
Cargo metadata, (b) is analogous to the approach used by most other
387-
languages, and (c) simplifies the implementation, since the parser
388-
does not need to be reconfigured midparse.
389-
390-
**Rather than supporting opt-in changes, one might consider simply
391-
issuing a new major release for every such change.** Put simply,
392-
though, issuing a new major release just because we want to have a new
393-
keyword feels like overkill. This seems like to have two potential
394-
negative effects. It may simply cause us to not make some of the
395-
changes we would make otherwise, or work harder to fit them within the
396-
existing syntactic constraints. It may also serve to dilute the
397-
meaning of issuing a new major version, since even additive changes
398-
that do not affect existing code in any meaningful way would result in
399-
a major release. One would then be tempted to have some *additional*
400-
numbering scheme, PR blitz, or other means to notify people when a new
401-
major version is coming that indicates deeper changes.
402-
403-
**Rather than simply fixing soundness bugs, we could use the opt-in
404-
mechanism to fix them conditionally.** This was initially considered
405-
as an option, but eventually rejected for the following reasons:
406-
407-
- This would effectively cause a deeper split between minor versions;
408-
currently, opt-in is limited to "surface changes" only, but allowing
409-
opt-in to affect the type system feels like it would be creating two
410-
distinct languages.
411-
- It seems likely that all users of Rust will want to know that their code
412-
is sound and would not want to be working with unsafe constructs or bugs.
413-
- Users may choose not to opt-in to newer versions because they do not
414-
need the new features introduced there or because they wish to
415-
preserve compatibility with older compilers. It would be sad for
416-
them to lose the benefits of bug fixes as well.
240+
**Rather than simply fixing soundness bugs, we could issue new major
241+
releases, or use some sort of opt-in mechanism to fix them
242+
conditionally.** This was initially considered as an option, but
243+
eventually rejected for the following reasons:
244+
245+
- Opting in to type system changes would cause deep splits between
246+
minor versions; it would also create a high maintenance burden in
247+
the compiler, since both older and newer versions would have to be
248+
supported.
249+
- It seems likely that all users of Rust will want to know that their
250+
code is sound and would not want to be working with unsafe
251+
constructs or bugs.
417252
- We already have several mitigation measures, such as opt-out or
418253
temporary deprecation, that can be used to ease the transition
419254
around a soundness fix. Moreover, separating out new type rules so
420255
that they can be "opted into" can be very difficult and would
421256
complicate the compiler internally; it would also make it harder to
422257
reason about the type system as a whole.
423258

424-
**Rather than using a version number to opt-in to minor changes, one
425-
might consider using the existing feature mechanism.** For example,
426-
one could write `#![feature(foo)]` to opt in to the feature "foo" and
427-
its associated keywords and type rules, rather than
428-
`#![rust_version="1.2.3"]`. While using minimum version numbers is
429-
more opaque than named features, they do offer several advantages:
430-
431-
1. Using a version number alone makes it easy to think about what
432-
version of Rust you are using as a conceptual unit, rather than
433-
choosing features "a la carte".
434-
2. Using named features, the list of features that must be attached to
435-
Rust code will grow indefinitely, presuming your crate wants to
436-
stay up to date.
437-
3. Using a version attribute preserves a mental separation between
438-
"experimental work" (feature gates) and stable, new features.
439-
4. Named features present a combinatoric testing problem, where we
440-
should (in principle) test for all possible combinations of
441-
features.
442-
443259
# Unresolved questions
444260

445-
**Can (and should) we give a more precise definition for compiler bugs
446-
and soundness problems?** The current text is vague on what precisely
447-
constitutes a compiler bug and soundness change. It may be worth
448-
defining more precisely, though likely this would be best done as part
449-
of writing up a more thorough (and authoritative) Rust reference
450-
manual.
451-
452-
**Should we add a mechanism for "escaping" keywords?"** We may need a
453-
mechanism for escaping keywords in the future. Imagine you have a
454-
public function named `foo`, and we add a keyword `foo`. Now, if you
455-
opt in to the newer version of Rust, your function declaration is
456-
illegal: but if you rename the function `foo`, you are making a
457-
breaking change for your clients, which you may not wish to do. If we
458-
had an escaping mechanism, you would probably still want to deprecate
459-
`foo` in favor of a new function `bar` (since typing `foo` would be
460-
awkward), but it could still exist.
461-
462-
**Should we add a mechanism for skipping over new syntax?** The
463-
current `#[cfg]` mechanism is applied *after* parsing. This implies
464-
that if we add new syntax, crates which employ that new syntax will
465-
not be parsable by older compilers, even if the modules that depend on
466-
that new syntax are disabled via `#[cfg]` directives. It may be useful
467-
to add some mechanism for informing the parser that it should skip
468-
over sections of the input (presumably based on token trees). One
469-
approach to this might just be modifying the existing `#[cfg]`
470-
directives so that they are applied during parsing rather than as a
471-
post-pass.
472-
473261
**What precisely constitutes "small" impact?** This RFC does not
474262
attempt to define when the impact of a patch is "small" or "not
475263
small". We will have to develop guidelines over time based on
@@ -478,23 +266,6 @@ observe on `crates.io` will be of the total breakage that will occur:
478266
it is certainly possible that all crates on `crates.io` work fine, but
479267
the change still breaks a large body of code we do not have access to.
480268

481-
**Should deprecation due to unsoundness have a special lint?** We may
482-
not want to use the same deprecation lint for unsoundness that we use
483-
for everything else.
484-
485-
**What attribute should we use to "opt out" of soundness changes?**
486-
The section on breaking changes indicated that it may sometimes be
487-
appropriate to includ an "opt out" that people can use to temporarily
488-
revert to older, unsound type rules, but did not specify precisely
489-
what that opt-out should look like. Ideally, we would identify a
490-
specific attribute in advance that will be used for such purposes. In
491-
the past, we have simply created ad-hoc attributes (e.g.,
492-
`#[old_orphan_check]`), but because custom attributes are forbidden by
493-
stable Rust, this has the unfortunate side-effect of meaning that code
494-
which opts out of the newer rules cannot be compiled on older
495-
compilers (even though it's using the older type system rules). If we
496-
introduce an attribute in advance we will not have this problem.
497-
498269
[RFC 1105]: https://github.com/rust-lang/rfcs/pull/1105
499270
[RFC 320]: https://github.com/rust-lang/rfcs/pull/320
500271
[#744]: https://github.com/rust-lang/rfcs/issues/744

0 commit comments

Comments
 (0)