<p>Alas, when developing my language I may have made some decisions that turned out less than ideal in hindsight. Some I managed to fix in a backwards compatible way, but others will require breaking changes.</p>
<p>I want to avoid a situation like Python 3 though where after 10 years still much of the ecosystem relies on the old version. Other languages have released many major versions without as much drama. What techniques can be used to speed up the transition period?</p>

<language-comparison>
I want to make breaking changes to my language, what techniques exist to allow a smooth transition of the ecosystem?
CC BY-SA 4.0
2023-07-13T17:02:53.097Z
Related question (but only applies to new keywords, not to all breaking changes): https://langdev.stackexchange.com/questions/2310/preserving-backwards-compatibility-when-adding-new-keywords
dan04
CC BY-SA 4.0
2023-07-13T17:08:46.933Z
@dan04 That question is about how to preserve backwards compatibility, this is for the situation when you have given up on preserving backwards compatibility
mousetail 'he-him'
CC BY-SA 4.0
2023-07-13T19:56:07.153Z
Is this about a hypothetical language or about a real one that is used commercially? For most research projects, simply changing things is the easiest way to continue.
feldentm
CC BY-SA 4.0
2023-07-14T00:09:17.080Z
highly related question: [What techniques have existing languages used or considered using to enable their evolution? Why were they chosen/considered? How have they fared?](/q/924/251)
starball
<p>A few more strategies.</p>
<h3>Editions</h3>
<p>Rust handles breaking changes via an edition mechanism.</p>
<p>First of all, unless otherwise <em>explicitly</em> specified, the edition is 2015 -- the first stable release of the language. Hence, infinite backward compatibility.</p>
<p>Secondly, to avoid the Python2/Python3 debacle, editions are restricted to <em>local</em> changes, mostly syntactic, so that the difference between editions of various libraries is <em>erased</em> by the front-end, and compiled libraries are <em>compatible</em> with each others.</p>
<p>It's a limited approach, but it works really well. Some old libraries are still in edition 2015, when there's been editions 2018 and 2021 already, and recent versions of the compiler can still compile them all in a single application.</p>
<h3>Fix-It</h3>
<p>If the ecosystem is responsive, <em>automating</em> the transition may be good enough. Go did it a lot in the early days, with <code>go fix</code> automatically &quot;fixing up&quot; your code to transition from old to new version.</p>
<p>It does require a new release of the library code -- hence the need for a responsive ecosystem -- but it takes little effort on each maintainer's thanks to the automation.</p>

CC BY-SA 4.0
2023-07-14T07:22:11.383Z
2023-07-14T07:22:11.383Z
Matthieu M.
<h2>Create a version before the breaking changes that is mostly forward compatible with them</h2>
<p>Fully updating a large code base takes a long time. However, feature requests and bugs do not go on pause and will also need attention during this time. This will lead to constant duplicate work and merge conflicts for those committed to performing the upgrade.</p>
<p>For companies, the upgrade project will tend not to get enough priority and the work needed to finish the process will only increase over time as the amount of new code grows faster than the amount of upgraded code. For open source projects there is also little motivation for doing duplicate work which also leads to these projects lasting a long time if they ever complete.</p>
<p>The solution is to offer a version of your language that has all the new replacement features but hasn't removed the old ones yet. That way, teams can gradually replace uses of the &quot;old way of doing things&quot; with &quot;the new way&quot; while still being able to pay attention to urgent features and bugs that may appear while they are working. New code can already be written with the new system so the amount of upgrade work will not increase over time.</p>
<p>Many languages follow this system, PHP for example normally have all the new features in the last minor version before the next major version.</p>
<p>This doesn't work for every type of feature, but any small part that can be finished before committing to a major upgrade makes the transition a little easier.</p>

CC BY-SA 4.0
2023-07-13T14:14:40.150Z
2023-07-13T20:56:58.267Z
2023-07-13T20:56:58.267Z
mousetail 'he-him'
CC BY-SA 4.0
2023-07-13T14:19:03.897Z
Swift has also done this to an extent: in Swift 5.5 and earlier, the places where you could use existential types were very limited, and there was no special marker. Swift 5.6 allowed you to mark them with `any`, Swift 5.7 added new situations where existentials are allowed and required the word `any` in those situations only, and then Swift 6.0 will require `any` for all existentials.
Bbrk24
CC BY-SA 4.0
2023-07-13T20:02:02.183Z
I do not see why Java is an example. Besides not having real minor versions, Java 8 seem to be more of a fork than an old version. Actually, Java 8 is a good counter example to your statement as lots of projects are stuck there and need to be replaced entirely.
feldentm
<blockquote>
<p>Other languages have released many major versions without as much drama. What techniques can be used to speed up the transition period?</p>
</blockquote>
<p>We thought about this A LOT on the C# design team. A few thoughts off the top of my head:</p>
<p>(1) Make your language really unpopular, or make the breaking change before the language is popular. If there's hardly any existing code, there's not much to break. If there are hardly any existing users, you can contact them directly and get them to patch their code.</p>
<p>This sounds silly but its true. I often think about how the precedence rules of <code>&amp;&amp;</code> and <code>||</code> are messed up in a great many languages descended from C because Ritchie didn't want to do a breaking change for the <em>dozens</em> of users C had at the time, and saddled literally millions of users with the consequences of that decision for decades to follow.</p>
<p>(2) A corollary of (1) that we used to great effect at Facebook: have control of 99.9% of the source code of your language. We introduced MANY breaking changes to Hack when I was on the Hack team. Since 99.9% of the Hack code in the world was in the same repo as the compiler, before we introduced a breaking change we would simply rewrite every piece of code that would have broken.</p>
<p>These work great but what if your language is in the unfortunate position of being enormously successful with millions of users and billions of lines of source code in the world, running key business processes that absolutely must not break?</p>
<p>(3) Don't do it. Bend over backwards to avoid breaking changes.</p>
<p>But sometimes it is inevitable.</p>
<p>(4) Make the breaking changes only affect code that is obscure, unlikely, or almost certainly wrong.</p>
<p>Examples from C#:</p>
<ul>
<li><p>In C# 1, <code>A(B&lt;C, D&gt;(E))</code> parses as &quot;call <code>A</code> with two arguments, <code>B&lt;C</code> and <code>D&gt;(E)</code>&quot;. In C# 2, it parses as &quot;call <code>A</code> with one argument, a call to generic method <code>B&lt;C, D&gt;</code> with argument <code>E</code>.</p>
<p>Did anyone actually write that code in C# 1?  Probably not. But still, the parser has a bunch of heuristics and lookahead rules that try to mitigate the impacts of these sorts of changes, just in case.</p>
</li>
<li><p><a href="https://stackoverflow.com/questions/8898925/is-there-a-reason-for-cs-reuse-of-the-variable-in-a-foreach/8899347#8899347">We took a breaking change so that &quot;access to modified closure&quot; stopped being a thing for <code>foreach</code> loops.</a></p>
<p>Anyone who was broken by that was almost certainly doing it wrong in the first place.</p>
</li>
</ul>
<p>And so on.</p>
<p>(5) Decide ahead of time what breaks are acceptable. Another example from C#, when we added lambdas in C# 3, the overload resolution rule was: if a lambda-to-delegate conversion would cause the body to have an error, the conversion is invalid:</p>
<pre class="lang-csharp prettyprint-override"><code>void M(Func&lt;int, int&gt; f) {}
void M(Func&lt;string, int&gt; f) {}
...
M( x =&gt; x.Length );
</code></pre>
<p>if <code>x</code> is <code>int</code>, <code>x.Length</code> is an error, not so if <code>x</code> is string, so overload resolution chooses the second overload.</p>
<p>But this rule is a factory for new breaking changes!  If we ever add a <code>Length</code> property to <code>int</code> then this legal program becomes an illegal program because overload resolution has no basis upon which to prefer one overload and gives an ambiguity error.  With this rule about lambdas, ANY change to ANY rule of C# or any new member of any type could potentially turn a working program into an ambiguous one, and we explicitly decided during the design of C# 3 that lambdas were worth that pain.</p>
<p>(6) Warn people ahead of time where breaks will happen, example, since day one the documentation for GetHashCode on System.String has said that Microsoft reserves the right to change the hash code for a given string at any time, for any reason.  But in .NET 1, the string hash code never changed, and people started using it as a key not in an in-memory hash table, but in DATABASE hash tables. When a later version of .NET used a different hash algorithm for strings, all those people broke, but they were forewarned if they'd read the documentation.</p>
<p>(7) That was still bad though. To prevent that from happening ever in the future within Microsoft, the hash algorithm was changed so that for debug builds of .NET, every build had a different string hashing algorithm. (The build number was the seed of the hash.)  Anyone who took a dependency on a thing that was likely to break got broken within 24 hours, not an entire version or two later.</p>
<p>(8) Pursue a multi-release strategy for breaking changes.  In version N, make use of the breaking feature a warning and recommend the replacement.  In version N+1, make it an error.  In version N+2, remove the original version entirely.  Having an <code>[Obsolete]</code> attribute or similar is useful.</p>
<p>There's a lot more to say here but that's all I have time for today; I hope that was helpful. Let me know if you have questions.</p>

CC BY-SA 4.0
2023-07-13T23:41:39.393Z
2023-07-15T13:42:25.740Z
2023-07-15T13:42:25.740Z
Eric Lippert
CC BY-SA 4.0
2023-07-14T01:45:59.847Z
The solution you stated in 7 is actually quite good. If you tell people explicitly that something WILL change, but they observe that it doesn't, then they ignore the documentation for some reason, and thinks that's how it is supposed to work. Making it actually change on every release would prevent them from doing something they know is wrong and is actually their own fault.
Nelson
CC BY-SA 4.0
2023-07-14T06:14:08.770Z
@Nelson case in point: the `dict` versus `OrderedDict` debate in python.
JAD
CC BY-SA 4.0
2023-07-14T06:25:38.257Z
@JAD Yeah, I've accidentally relied on ordered dict couple times, but refactored when going through the specification. I just don't know why people ignore documented info saying "it is unordered" and then complain when it does exactly what it says it does.
Nelson
CC BY-SA 4.0
2023-07-14T07:14:36.963Z
@Nelson: Bold of you to assume people read the documentation ;) (Which is why, of course, point 7 is an excellent point)
Matthieu M.
CC BY-SA 4.0
2023-07-14T10:47:11.780Z
"I often think about how the precedence rules of `&&` and `||` are messed up in a great many languages descended from C" – It looks to me like in C, `&&` binds more tightly than `||`, which is exactly what I would expect. I do notice, though, that `==` binds more tightly than the bitwise operators `&`, `|`, and `^`, which definitely _is_ counterintuitive—an expression like `x & 3 == 2` will parse as `x & (3 == 2)` instead of `(x & 3) == 2`. Is that perhaps what you were thinking of?
Sophie Swett
CC BY-SA 4.0
2023-07-14T16:53:09.880Z
Interestingly, SQLite adopted C's bitwise operators [but changed the precedence](https://www.sqlite.org/lang_expr.html#operators_and_parse_affecting_attributes) to be higher than relationals, allowing typical bitmasking comparisons to use fewer parentheses.
dan04
CC BY-SA 4.0
2023-07-14T23:09:56.950Z
Is it ever practical to write an automated migration tool, which uses the old parser and "compiles" to the new language?
Daniel Wagner
CC BY-SA 4.0
2023-07-15T01:22:34.120Z
@TannerSwett: Yes, that's exactly it.
Eric Lippert
CC BY-SA 4.0
2023-07-15T01:23:39.450Z
@DanielWagner: We did a variation of this at Facebook, yes. We used the compiler's parser to also build a search-and-replace tool that could rapidly find instances of to-be-deprecated patterns in the many-million-LOC repo.
Eric Lippert
CC BY-SA 4.0
2023-07-16T19:15:53.623Z
I wonder how long before compiler branches of C# happen because of a handful of missing features.
Joshua
CC BY-SA 4.0
2023-07-17T19:42:44.583Z
@Joshua: The language design team is keen to allow experimentation but is also concerned that the language not split into mutually incompatible fragments. We observed over a dozen wrong C# parser/analyzers just within Microsoft before Roslyn, and wanted to make sure there was one easy-to-use source of truth; hopefully that compelling benefit discourages splintering while encouraging experimentation.
Eric Lippert
CC BY-SA 4.0
2023-07-17T20:26:24.217Z
@EricLippert: For reference, we had a debate about straight up forking the VB.NET compiler over the `<Obsolete(true)>` on `Span` and just knock the check out so it would compile anyway. The code can't convert to C# ever because of named indexed properties.
Joshua
<h2>Make breaking changes opt-in ahead of time</h2>
<p>This is the route taken by Swift and Python, for example. Python lets you do this at the source level, per-file, with the special form</p>
<pre class="lang-py prettyprint-override"><code>from __future__ import breaking_change
</code></pre>
<p>where <code>breaking_change</code> is the name of an upcoming change that is not yet enabled by default. Swift does this per-module with the compiler flag <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md" rel="noreferrer"><code>-enable-upcoming-feature</code></a>.</p>
<p>Using this approach means that projects can incrementally enable new features as they update the code. It's also possible to alert users of a breaking change without rejecting the code <em>yet</em>: some compiler warnings in Swift 5 warn that &quot;this is an error in Swift 6.&quot;</p>

CC BY-SA 4.0
2023-07-13T14:25:49.860Z
2023-07-13T14:25:49.860Z
Bbrk24
CC BY-SA 4.0
2023-07-13T20:04:20.993Z
While such approaches are generally a good idea, they can be hard to implement and have very negative consequences on the overall compiler architecture. Also, the analysis itself can be expensive but having it optional is likely a bad idea.
feldentm
CC BY-SA 4.0
2023-07-15T00:53:54.007Z
Java uses the same commandline flag approach for preview features
Seggan
CC BY-SA 4.0
2023-07-24T15:19:01.793Z
@feldentm: Some kinds of compilation-mode changes may be awkward to process efficiently, but even many tricky cases could be handled by having code pass through an initial step which is based upon an old parser, and slightly reformats code or adds annotations so as to work with a new one.  For example, if an old version of a language specified that applying the "pasting" operator to "+" and "++" would yield "++" and "+" (performing string concatenation and re-tokenizing), a first-phase program could process things in that manner but add annotations to prevent...
supercat
CC BY-SA 4.0
2023-07-24T15:19:20.980Z
...things like macro substitutions from being improperly repeated in the main compiler.
supercat
<h2>Living on head, parsing immediate past</h2>
<p>Or plan to break <em>and support</em> from onset. That is, and to use the Python example, say that any <code>.py</code> file is &quot;current&quot; Python version, but your compilar can accepts and compile older <code>.py1</code> or <code>.py2</code> source files, that will have the older functionality somewhat emulated in new semantics.</p>
<p>This will work for <em>major</em> versions, but breaking changes in the level of new keywords or complete new and breaking <code>string</code> type may warrant a major release anyways.</p>
<p>For smaller changes, a directive or <code>pragma</code> may indicate that file in particular is following a specific major-minor release of language.</p>
<p>Something like:</p>
<pre><code>from __past__ import lang.3.4
// or
#pragma langversion 3.4
</code></pre>
<p>Your new compiler version can then emit warnings or rejections about that old code. You will need to support only two immediate versions of language, as any programmer can use each successive version of the compiler to upgrade the code.</p>
<p>As a new major or minor version is released, the (now) old code upgrade is released as a separated &quot;lint&quot;, or even better, as source-to-source compiler. With these &quot;upgrade tools&quot;, your user base will have a very incentive to leave the past in the past.</p>
<h2>Have a <em>very</em> good LSP for your language</h2>
<p>I have a friend whose programming skills got rusty and asked me to work on a pair programming hobby project to remedy this. For this project, I selected a somewhat old utility that both of us use.</p>
<p>Why is this relevant? Because the project is C# language, originally written half a decade ago. We settleted to using VSCode, and the old code, opened in the new IDE, showed a lot of null warnings and suggestions of how to fix these, and how to use more idiomatic ways to manipulate substrings, records, enumerations...</p>
<p><em>I'm learning a lot, while fixing and upgrading these things, a warning or tip at time.</em>. I'm literally upgrading the code, and not even thinking in these terms.</p>
<p>There is something when code upgrating is a <em>pleasurable</em> experience.</p>

CC BY-SA 4.0
2023-07-14T14:39:24.827Z
2023-07-14T16:31:53.920Z
2023-07-14T16:31:53.920Z
André L F S Bacci
CC BY-SA 4.0
2023-07-18T12:17:26.447Z
+1 for the 'pleasurable experience' idea!
Pablo H
# I want to make breaking changes to my language, what techniques exist to allow a smooth transition of the ecosystem?
Posted by: username_0 on 2023-07-13
Alas, when developing my language I may have made some decisions that turned out less than ideal in hindsight. Some I managed to fix in a backwards compatible way, but others will require breaking changes.

I want to avoid a situation like Python 3 though where after 10 years still much of the ecosystem relies on the old version. Other languages have released many major versions without as much drama. What techniques can be used to speed up the transition period?
**Tags:** language-comparison
> **Comments:**
> **username_1**: Related question (but only applies to new keywords, not to all breaking changes): https://langdev.stackexchange.com/questions/2310/preserving-backwards-compatibility-when-adding-new-keywords  
> **username_0**: @dan04 That question is about how to preserve backwards compatibility, this is for the situation when you have given up on preserving backwards compatibility  
> **username_2**: Is this about a hypothetical language or about a real one that is used commercially? For most research projects, simply changing things is the easiest way to continue.  
> **username_3**: highly related question: [What techniques have existing languages used or considered using to enable their evolution? Why were they chosen/considered? How have they fared?](/q/924/251)  
---
## 5 answers
### Answer by username_6 on 2023-07-13. Score: 51
Other languages have released many major versions without as much drama. What techniques can be used to speed up the transition period?

We thought about this A LOT on the C# design team. A few thoughts off the top of my head:
(1) Make your language really unpopular, or make the breaking change before the language is popular. If there's hardly any existing code, there's not much to break. If there are hardly any existing users, you can contact them directly and get them to patch their code.
This sounds silly but its true. I often think about how the precedence rules of && and || are messed up in a great many languages descended from C because Ritchie didn't want to do a breaking change for the dozens of users C had at the time, and saddled literally millions of users with the consequences of that decision for decades to follow.
(2) A corollary of (1) that we used to great effect at Facebook: have control of 99.9% of the source code of your language. We introduced MANY breaking changes to Hack when I was on the Hack team. Since 99.9% of the Hack code in the world was in the same repo as the compiler, before we introduced a breaking change we would simply rewrite every piece of code that would have broken.
These work great but what if your language is in the unfortunate position of being enormously successful with millions of users and billions of lines of source code in the world, running key business processes that absolutely must not break?
(3) Don't do it. Bend over backwards to avoid breaking changes.
But sometimes it is inevitable.
(4) Make the breaking changes only affect code that is obscure, unlikely, or almost certainly wrong.
Examples from C#:

In C# 1, A(B(E)) parses as "call A with two arguments, B and D>(E)". In C# 2, it parses as "call A with one argument, a call to generic method B with argument E.
Did anyone actually write that code in C# 1?  Probably not. But still, the parser has a bunch of heuristics and lookahead rules that try to mitigate the impacts of these sorts of changes, just in case.

We took a breaking change so that "access to modified closure" stopped being a thing for foreach loops.
Anyone who was broken by that was almost certainly doing it wrong in the first place.


And so on.
(5) Decide ahead of time what breaks are acceptable. Another example from C#, when we added lambdas in C# 3, the overload resolution rule was: if a lambda-to-delegate conversion would cause the body to have an error, the conversion is invalid:
void M(Func f) {}
void M(Func f) {}
...
M( x => x.Length );

if x is int, x.Length is an error, not so if x is string, so overload resolution chooses the second overload.
But this rule is a factory for new breaking changes!  If we ever add a Length property to int then this legal program becomes an illegal program because overload resolution has no basis upon which to prefer one overload and gives an ambiguity error.  With this rule about lambdas, ANY change to ANY rule of C# or any new member of any type could potentially turn a working program into an ambiguous one, and we explicitly decided during the design of C# 3 that lambdas were worth that pain.
(6) Warn people ahead of time where breaks will happen, example, since day one the documentation for GetHashCode on System.String has said that Microsoft reserves the right to change the hash code for a given string at any time, for any reason.  But in .NET 1, the string hash code never changed, and people started using it as a key not in an in-memory hash table, but in DATABASE hash tables. When a later version of .NET used a different hash algorithm for strings, all those people broke, but they were forewarned if they'd read the documentation.
(7) That was still bad though. To prevent that from happening ever in the future within Microsoft, the hash algorithm was changed so that for debug builds of .NET, every build had a different string hashing algorithm. (The build number was the seed of the hash.)  Anyone who took a dependency on a thing that was likely to break got broken within 24 hours, not an entire version or two later.
(8) Pursue a multi-release strategy for breaking changes.  In version N, make use of the breaking feature a warning and recommend the replacement.  In version N+1, make it an error.  In version N+2, remove the original version entirely.  Having an [Obsolete] attribute or similar is useful.
There's a lot more to say here but that's all I have time for today; I hope that was helpful. Let me know if you have questions.
> **Comments:**
> **username_7** (2023-07-14): The solution you stated in 7 is actually quite good. If you tell people explicitly that something WILL change, but they observe that it doesn't, then they ignore the documentation for some reason, and thinks that's how it is supposed to work. Making it actually change on every release would prevent them from doing something they know is wrong and is actually their own fault.  
> **username_8** (2023-07-14): @Nelson case in point: the `dict` versus `OrderedDict` debate in python.  
> **username_7** (2023-07-14): @JAD Yeah, I've accidentally relied on ordered dict couple times, but refactored when going through the specification. I just don't know why people ignore documented info saying "it is unordered" and then complain when it does exactly what it says it does.  
> **username_4** (2023-07-14): @Nelson: Bold of you to assume people read the documentation ;) (Which is why, of course, point 7 is an excellent point)  
> **username_9** (2023-07-14): "I often think about how the precedence rules of `&&` and `||` are messed up in a great many languages descended from C" – It looks to me like in C, `&&` binds more tightly than `||`, which is exactly what I would expect. I do notice, though, that `==` binds more tightly than the bitwise operators `&`, `|`, and `^`, which definitely _is_ counterintuitive—an expression like `x & 3 == 2` will parse as `x & (3 == 2)` instead of `(x & 3) == 2`. Is that perhaps what you were thinking of?  
> **username_1** (2023-07-14): Interestingly, SQLite adopted C's bitwise operators [but changed the precedence](https://www.sqlite.org/lang_expr.html#operators_and_parse_affecting_attributes) to be higher than relationals, allowing typical bitmasking comparisons to use fewer parentheses.  
> **username_10** (2023-07-14): Is it ever practical to write an automated migration tool, which uses the old parser and "compiles" to the new language?  
> **username_6** (2023-07-15): @TannerSwett: Yes, that's exactly it.  
> **username_6** (2023-07-15): @DanielWagner: We did a variation of this at Facebook, yes. We used the compiler's parser to also build a search-and-replace tool that could rapidly find instances of to-be-deprecated patterns in the many-million-LOC repo.  
> **username_11** (2023-07-16): I wonder how long before compiler branches of C# happen because of a handful of missing features.  
> **username_6** (2023-07-17): @Joshua: The language design team is keen to allow experimentation but is also concerned that the language not split into mutually incompatible fragments. We observed over a dozen wrong C# parser/analyzers just within Microsoft before Roslyn, and wanted to make sure there was one easy-to-use source of truth; hopefully that compelling benefit discourages splintering while encouraging experimentation.  
> **username_11** (2023-07-17): @EricLippert: For reference, we had a debate about straight up forking the VB.NET compiler over the `<Obsolete(true)>` on `Span` and just knock the check out so it would compile anyway. The code can't convert to C# ever because of named indexed properties.  
---
### Answer by username_5 on 2023-07-13. Score: 21
Make breaking changes opt-in ahead of time
This is the route taken by Swift and Python, for example. Python lets you do this at the source level, per-file, with the special form
from __future__ import breaking_change

where breaking_change is the name of an upcoming change that is not yet enabled by default. Swift does this per-module with the compiler flag -enable-upcoming-feature.
Using this approach means that projects can incrementally enable new features as they update the code. It's also possible to alert users of a breaking change without rejecting the code yet: some compiler warnings in Swift 5 warn that "this is an error in Swift 6."
> **Comments:**
> **username_2** (2023-07-13): While such approaches are generally a good idea, they can be hard to implement and have very negative consequences on the overall compiler architecture. Also, the analysis itself can be expensive but having it optional is likely a bad idea.  
> **username_12** (2023-07-15): Java uses the same commandline flag approach for preview features  
> **username_13** (2023-07-24): @feldentm: Some kinds of compilation-mode changes may be awkward to process efficiently, but even many tricky cases could be handled by having code pass through an initial step which is based upon an old parser, and slightly reformats code or adds annotations so as to work with a new one.  For example, if an old version of a language specified that applying the "pasting" operator to "+" and "++" would yield "++" and "+" (performing string concatenation and re-tokenizing), a first-phase program could process things in that manner but add annotations to prevent...  
> **username_13** (2023-07-24): ...things like macro substitutions from being improperly repeated in the main compiler.  
---
### Answer by username_4 on 2023-07-14. Score: 11
A few more strategies.

### Editions

Rust handles breaking changes via an edition mechanism.

First of all, unless otherwise *explicitly* specified, the edition is 2015 -- the first stable release of the language. Hence, infinite backward compatibility.

Secondly, to avoid the Python2/Python3 debacle, editions are restricted to *local* changes, mostly syntactic, so that the difference between editions of various libraries is *erased* by the front-end, and compiled libraries are *compatible* with each others.

It's a limited approach, but it works really well. Some old libraries are still in edition 2015, when there's been editions 2018 and 2021 already, and recent versions of the compiler can still compile them all in a single application.

### Fix-It

If the ecosystem is responsive, *automating* the transition may be good enough. Go did it a lot in the early days, with `go fix` automatically "fixing up" your code to transition from old to new version.

It does require a new release of the library code -- hence the need for a responsive ecosystem -- but it takes little effort on each maintainer's thanks to the automation.
---
### Answer by username_0 on 2023-07-13. Score: 9
## Create a version before the breaking changes that is mostly forward compatible with them

Fully updating a large code base takes a long time. However, feature requests and bugs do not go on pause and will also need attention during this time. This will lead to constant duplicate work and merge conflicts for those committed to performing the upgrade.

For companies, the upgrade project will tend not to get enough priority and the work needed to finish the process will only increase over time as the amount of new code grows faster than the amount of upgraded code. For open source projects there is also little motivation for doing duplicate work which also leads to these projects lasting a long time if they ever complete.

The solution is to offer a version of your language that has all the new replacement features but hasn't removed the old ones yet. That way, teams can gradually replace uses of the "old way of doing things" with "the new way" while still being able to pay attention to urgent features and bugs that may appear while they are working. New code can already be written with the new system so the amount of upgrade work will not increase over time.

Many languages follow this system, PHP for example normally have all the new features in the last minor version before the next major version.

This doesn't work for every type of feature, but any small part that can be finished before committing to a major upgrade makes the transition a little easier.
> **Comments:**
> **username_5** (2023-07-13): Swift has also done this to an extent: in Swift 5.5 and earlier, the places where you could use existential types were very limited, and there was no special marker. Swift 5.6 allowed you to mark them with `any`, Swift 5.7 added new situations where existentials are allowed and required the word `any` in those situations only, and then Swift 6.0 will require `any` for all existentials.  
> **username_2** (2023-07-13): I do not see why Java is an example. Besides not having real minor versions, Java 8 seem to be more of a fork than an old version. Actually, Java 8 is a good counter example to your statement as lots of projects are stuck there and need to be replaced entirely.  
---
### Answer by username_14 on 2023-07-14. Score: 4
Living on head, parsing immediate past
Or plan to break and support from onset. That is, and to use the Python example, say that any .py file is "current" Python version, but your compilar can accepts and compile older .py1 or .py2 source files, that will have the older functionality somewhat emulated in new semantics.
This will work for major versions, but breaking changes in the level of new keywords or complete new and breaking string type may warrant a major release anyways.
For smaller changes, a directive or pragma may indicate that file in particular is following a specific major-minor release of language.
Something like:
from __past__ import lang.3.4
// or
#pragma langversion 3.4

Your new compiler version can then emit warnings or rejections about that old code. You will need to support only two immediate versions of language, as any programmer can use each successive version of the compiler to upgrade the code.
As a new major or minor version is released, the (now) old code upgrade is released as a separated "lint", or even better, as source-to-source compiler. With these "upgrade tools", your user base will have a very incentive to leave the past in the past.
Have a very good LSP for your language
I have a friend whose programming skills got rusty and asked me to work on a pair programming hobby project to remedy this. For this project, I selected a somewhat old utility that both of us use.
Why is this relevant? Because the project is C# language, originally written half a decade ago. We settleted to using VSCode, and the old code, opened in the new IDE, showed a lot of null warnings and suggestions of how to fix these, and how to use more idiomatic ways to manipulate substrings, records, enumerations...
I'm learning a lot, while fixing and upgrading these things, a warning or tip at time.. I'm literally upgrading the code, and not even thinking in these terms.
There is something when code upgrating is a pleasurable experience.
> **Comments:**
> **username_15** (2023-07-18): +1 for the 'pleasurable experience' idea!  
---
