5
5
6
6
# Summary
7
7
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.
14
11
15
12
# Motivation
16
13
@@ -29,29 +26,11 @@ disruptive to the ecosystem. Therefore, **the RFC also proposes
29
26
specific measures to mitigate the impact of breaking changes** , and
30
27
some criteria when those measures might be appropriate.
31
28
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.
55
34
56
35
# Detailed design
57
36
@@ -247,229 +226,38 @@ future as well. The `-Z` flags are of course explicitly unstable, but
247
226
some of the ` -C ` , rustdoc, and linker-specific flags are expected to
248
227
evolve over time (see e.g. [ #24451 ] ).
249
228
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
-
361
229
# Drawbacks
362
230
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.
377
235
378
236
## Notes on phasing
379
237
380
238
# Alternatives
381
239
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.
417
252
- We already have several mitigation measures, such as opt-out or
418
253
temporary deprecation, that can be used to ease the transition
419
254
around a soundness fix. Moreover, separating out new type rules so
420
255
that they can be "opted into" can be very difficult and would
421
256
complicate the compiler internally; it would also make it harder to
422
257
reason about the type system as a whole.
423
258
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
-
443
259
# Unresolved questions
444
260
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
-
473
261
** What precisely constitutes "small" impact?** This RFC does not
474
262
attempt to define when the impact of a patch is "small" or "not
475
263
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:
478
266
it is certainly possible that all crates on ` crates.io ` work fine, but
479
267
the change still breaks a large body of code we do not have access to.
480
268
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
-
498
269
[ RFC 1105 ] : https://github.com/rust-lang/rfcs/pull/1105
499
270
[ RFC 320 ] : https://github.com/rust-lang/rfcs/pull/320
500
271
[ #744 ] : https://github.com/rust-lang/rfcs/issues/744
0 commit comments