-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
WIP: Add a loading mechanism for compat-dependent API sets #59995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This is a first WIP implementation for providing the underlying mechanism for a #54905. I haven't fully designed out what I want the final thing to look like, but I've thought about it enough that I think we need some hands-on examples for further design exploration, which is what this PR is designed to support. The core idea here is that if you specify in your package ``` [compact] julia = "0.14" MyDependency = "0.3,0.4" ``` Then this mechanism will ask `MyDependency` to provide you a view of its API that is compatible with its 0.3 version, even if 0.4 is installed. The objective of this mechanism is that downstreams of widely used packages (including `Base` and standard libraries) can be upgraded more gradually by allowing some portion of the packages in project to use the new 0.4 API set, while packages that have not yet upgraded can continue to use the 0.3 API set. Currently, whenever a widely used package changes an API there's a big frenzied rush to update downstreams, which just isn't sustainable as the ecosystem grows. In other languages, problems like these are solved by allowing multiple versions of the same package to be loaded. However, in julia, this is not particularly feasible by default because packages may have generic functions that need to be extended and unified and which break if there are multiple copies of such resources. That said, for simple packages, this mechanism could be used to emulate the multiple-version-loading strategy on an opt-in basis. The way that this works is that `loading` gains an additional layer of indirection that is provided the `compat` specification of the loading package. Packages can opt into this by providing a ``` function _get_versioned_api(compat) end ``` function. Where the default is equivalent to `_get_versioned_api(compat) = @__MODULE__`. The loader makes no further assumptions on either the structure of `compat` or how the package processes it. This is intentional to allow evolution without impacting the core loading logic (of course Pkg also looks at the structure of compat, as would the `_get_versioned_api` implementation in Base, so there are some constraints). That said, the envisioned usage of this is that we create a standard (in the sense of being widely used, not in the sense of it being a stdlib) package to help package authors define the APIs of their packages more precisely and with version information. This package would then create a set of modules under the hood that describe these APIs to the sytem and would set up `_get_versioned_api` appropriately (i.e. the mechanism is not intended to be used directly in most cases). To actually make this useful, we will likely need some additional features in the binding system, in particular: 1. #59859 and a few variants thereon to make the API modueles look more transparent. 2. A mechanism to intercept extension of generic function overloads in order to be able to provide compatibility (Design/PR for this forthcoming). Note that this PR itself is WIP, it is not yet fully complete and there is also a Pkg.jl side of this required to put compat info into the manifest.
4e632b8 to
7caf143
Compare
|
Just to check my understanding, this effectively means that what module you get from Would this make it harder for static analyzers, since they AFAIU have to be able to run the Also, I am worried it will also be confusing to humans since you always have to have that compat context available (and run the There also needs to be extra registry information here to say that a semantically breaking version bump should not be considered breaking, IIUC? |
What module the
Kind of - they could probably just ignore it and resolve the minimum set. Depends on what they're trying to do. We already have various callback mechanisms in loading though, so I don't think this ups the code execution burden for a full loading implementation.
I think that is true, but as you point out, could already be the case now. I think the alternative to think about here loading multiple versions of the module, which would have the same problem.
I was not planning to do this. Rather, I was expecting packages to start using minor version bumps for changed APIs and only use major version bumps when removing the compat code. |
|
Are these compat modules actually Extensions? It feels like the same sort of conditional code loading based on some external state. But it feels slightly backwards in some way that I can't quite put my finger on. I don't really like that I would now need to know two things in order to understand what |
No, there is no conditional loading.
Well, I'd argue the most important thing to know (even now) is which versions of
If example does not use this mechanism nothing changes.
You would not use this mechanism and instead put a versioned import behind a version check (which is outside the scope of this PR). |
Yeah, that's the part I'm struggling with, I suppose, combined with packages not using the mechanism at all. Because currently, it's entirely the responsibility of the downstream author to ensure that their I know, I know, you're gonna say that's all intentionally out of scope, and I get that this is intentionally very flexible... but this feels wildly flexible. |
Well, the dependency would tag a new version of itself. I don't think this is any different from tagging a new patch version now that unintentionally breaks an API.
As I said, it is not the intention that package authors will use this mechanism directly (there are some corner cases where they may want to, e.g. for packages that provide FFI bridges and want to emulate by loading multiple package versions in the other language). There are some syntax proposal for defining API sets in the original issue, but that's a separate concern from the mechanism here. I do think erring on the side of flexibilty in the loader here is the right call. We could of course define some complicated data structure to declare API sets in base and just parse that here, but then we're stuck with it., |
|
I think as we discuss this there are a couple of things we should separate out.
I think the discussion will be more helpful if these are separated out. |
There are several corner cases in the Julia syntax that are essentially bugs or mistakes that we'd like to possibly remove, but can't due to backwards compatibility concerns. Similarly, when adding new syntax features, there are often cases that overlap with valid (but often nonsensical) existing syntax. In the past, we've mostly done judegement calls of these being "minor changes", but as the package ecosystem grows, so does the chance of someone accidentally using these anyway and our "minor changes" have (subjectively) resulted in more breakages recently. Fortunately, all the recent work on making the parser replacable, combined with the fact that JuliaSyntax already supports parsing multiple revisions of Julia syntax provides a solution here: Just let packages declare what version of the Julia syntax they are using. That way, packages would not break if we make changes to the syntax and they can be upgraded at their own pace the next time the author of that particular package upgrades to a new julia version. The way this works is simple. Right now, the parser function is always looked up in `Core._parse`. With this PR, it is instead looked up as `rootmodule(mod)._internal_julia_parse` (slightly longer name to avoid conflicting with existing bindings of the name in downstream packages), or `Core._parse` if no such binding exists. Similar for `_lower`. At the moment, the supported way to make this election is to write `@Base.Experimental.set_syntax_version v"1.14"` (or whatever the version is that you're writing your syntax against). However, to make this truly smooth, I think this should happen automatically through a Project.toml opt-in specifying the expected syntax version. My preference would be to use #59995 if that is merged, but this is a separate feature (with similar motivations around API evolution of course) and there could be a different opt-in mechanism. I should emphasize that I'm not proposing using this for any big syntax revolutions or anything. I would just like to start cleaning up a few corners of the syntax that I think are universally agreed to be bad but that we've kept for backwards compatibility. This way, by the time we get around to making a breaking revision, our entire ecosystem will have already upgraded to the new syntax.
|
In offline discussion, there is some skepticism that this mechanism is necessary. In particular, there is a concern that the mechanism being available by default would suggest to package developers that they must use it, as opposed to it being a tool for package developers with large downstream dependency chains who want to maintain backwards compatibility. Thus the plan is not to merge this and instead focus on providing the requisite functionality in a package. Users would have to opt-in to versioned APIs with a macro, but that's likely acceptable and can still read the compat. We can revisit making this automatic in a future julia version if the concept is well accepted. |
This is a first WIP implementation for providing the underlying mechanism for #54905. I haven't fully designed out what I want the final thing to look like, but I've thought about it enough that I think we need some hands-on examples for further design exploration, which is what this PR is designed to support.
The core idea here is that if you specify in your package
Then this mechanism will ask
MyDependencyto provide you a view of its API that is compatible with its 0.3 version, even if 0.4 is installed. The objective of this mechanism is that downstreams of widely used packages (includingBaseand standard libraries) can be upgraded more gradually by allowing some portion of the packages in project to use the new 0.4 API set, while packages that have not yet upgraded can continue to use the 0.3 API set. Currently, whenever a widely used package changes an API there's a big frenzied rush to update downstreams, which just isn't sustainable as the ecosystem grows.In other languages, problems like these are solved by allowing multiple versions of the same package to be loaded. However, in julia, this is not particularly feasible by default because packages may have generic functions that need to be extended and unified and which break if there are multiple copies of such resources.
That said, for simple packages, this mechanism could be used to emulate the multiple-version-loading strategy on an opt-in basis.
The way that this works is that
loadinggains an additional layer of indirection that is provided thecompatspecification of the loading package. Packages can opt into this by providing afunction. Where the default is equivalent to
_get_versioned_api(compat) = @__MODULE__.The loader makes no further assumptions on either the structure of
compator how the package processes it, other than that it must return aModule. This is intentional to allow evolution without impacting the core loading logic (of course Pkg also looks at the structure of compat, as would the_get_versioned_apiimplementation in Base, so there are some constraints).That said, the envisioned usage of this is that we create a standard (in the sense of being widely used, not in the sense of it being a stdlib) package to help package authors define the APIs of their packages more precisely and with version information. This package would then create a set of modules under the hood that describe these APIs to the system and would set up
_get_versioned_apiappropriately (i.e. the mechanism is not intended to be used directly in most cases).To actually make this useful, we will likely need some additional features in the binding system, in particular:
bindings: Add native automatic re-export feature #59859 and a few variants thereon to make the API modueles look more transparent.
A mechanism to intercept extension of generic function overloads in order to be able to provide compatibility (Design/PR for this forthcoming).
Note that this PR itself is WIP, it is not yet fully complete and there is also a Pkg.jl side of this required to put compat info into the manifest.