language icon indicating copy to clipboard operation
language copied to clipboard

SDK library versioning by language version

Open lrhn opened this issue 3 years ago • 4 comments

There is currently no way to version libraries in the same way that we version the Dart language.

A library has a single public API, no matter the language version of the code which looks at it. That means that removing a deprecated feature becomes a backwards breaking operation that can only be done in major version increments of the Dart SDK, and that makes major version increments harder introduce because existing code may need to migrate to use the new SDK, even if the new SDK supports the older language versions used by the existing code. There is no gradual migration.

Doing proper versioning, where the same code has multiple different and incompatible APIs, depending on who's looking, is incredibly hard to do.

However, some feature removals can be approximated by not actually removing the feature, but still making it a compile-time error to depend on the feature from code with in unsupported language versions.

Proposal

We introduce an annotation which marks a particular API entity as inaccessible/invisible for specific language versions.

Strawman:

@pragma("dart-sdk:library-versioning", "<3.0") //   - Declaration is invisible in language versions >= 3.0

@pragma("dart-sdk:library-versioning", ">=3.0") //  - Declaration is invisible in language versions < 3.0

The annotation can applies any top-level or static declaration, and to trailing positional or named optional parameters of static/top-level method and constructor parameters.

If an annotated declaration is accessed by a library with a language version not included in the accepted range, the name resolution gives an error (it's still there, because otherwise it might remove a name conflict, but it's treated like a "prohibited access"). For an optional parameter, the static function type of the declaration is the type without such parameters.

That means that the annotation actually affects semantics, by exposing a different static type to some libraries.

The Dart language version 2.12 List constructor tweak (the List constructor cannot be used from null safe code) is effectively what the annotation:

@pragma("dart-sdk:library-versioning", "<2.12")

would provide.

It's not possible to remove/hide instance members, since this is only static-type and rejection-based, it doesn't actually change the runtime types of anything. (You can tear off a method with a hidden optional parameter, cast it to dynamic or a type with the parameter, and call it anyway.)

lrhn avatar Aug 24 '22 16:08 lrhn

Are we concerned about any potential impact to the size of kernel files based on this change? Presumably there would be an additional optional field attached to each and every static API?

jakemac53 avatar Aug 24 '22 16:08 jakemac53

However, some feature removals can be approximated by not actually removing the feature but still making it a compile-time error to depend on the feature from code within unsupported language versions.

@pragma("dart-sdk:library-versioning", "<3.0") //   - Declaration is invisible in language versions >= 3.0

How about adding a version field to Deprecated? The analyzer could check the package version and compare it to the version field to decide if it should show the lint. In the case of Dart SDK libraries, that could be the user's current version of Dart.

@Deprecated("Use MyOtherClass instead", version: "3.0")
class MyClass { }  // Using this in Dart >= 3.0 surfaces the lint
@pragma("dart-sdk:library-versioning", ">=3.0") //  - Declaration is invisible in language versions < 3.0

How about another annotation that's the reverse of Deprecated -- it disallows you to use a member before a certain version. Normally, that's not a problem for packages since you can only use a member if declared, but for the Dart SDK, you can use // @dart=x.y, so it's a little different. Something like

@FutureFeature("3.0")

Levi-Lesches avatar Aug 24 '22 21:08 Levi-Lesches

@Levi-Lesches I don't think this idea works for packages in general. Packages just release a new version without the feauture, and clients can upgrade or not. (It's not that simple, I know, but that's the intended model.) If a package added @Deprecated("something", version: "3.0"), there is no big reason that it should depend on language version. It either works, or it's likely invalid code in the new Dart language version. If it should trigger on anything, it should be the minimum constraint on that package in the pubspec, then it would allow you to "remove" API in a release without making that version incompatible with code expecting an earlier version.

The Dart SDK is special in that you don't control which version your code gets run with. And the Dart SDK supports backwards compatibility with older SDK releases at the language level, because of that, but not the library level. This is an attempt to provide (limited) backwards compatibility at the library level as well, without actually needing to have separate platform library code for code expecting different SDK versions.

If it's SDK only, putting something on Deprecated is a mistake. (And I definitely don't want the parameter behavior, where the annotation affects the static type, to work on anything which is not an SDK library. Annotations should not affect semantics, and doing it for platform libraries is a hack that we can at least contain the scope of.)

All in all, I'm not trying to make a general "conditional compilation" feature, just a hack to allow easier migration for SDK libraries, to lessen the self-imposed burdened by backwards compatibility.

lrhn avatar Aug 25 '22 09:08 lrhn

The size of kernel can probably be reduced by keeping the information on the side, as a kind of metadata. Say a list of "Library:name:name:name=>version" entries specifying declarations and version constraints, which code which knows about it can choose to read. Everybody else can ignore it.

lrhn avatar Aug 25 '22 09:08 lrhn