-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
cli: Add infrastructure for new CLI drivers juliax
and juliac
#51417
base: master
Are you sure you want to change the base?
Conversation
Following the discussion in #50974, it became clear that there is significant objection to changes to the behavior of the julia CLI driver. Some commenters found changes to the behavior acceptable if they were unlikely to impact existing code (e.g. in the case of #50974 using `__main__` instead of `main`), while other were worried about the reputational risks of even changing behavior in the corner case. In subsequent discussion it became relatively clear that the only way forward that did not raise significant objection was to introduce a new CLI driver for the new behavior. This may seem a bit drastic just for the change in #50974, but I actually think there's a number of other quality-of-life improvements that this would enable us to do over time, for example: - Autoloading/caching of all packages in the manifest - auto-selection of julia versions (integrate juliaup?) In addition, it doesn't seem so bad to have some CLI design flexibility to make sure that the `juliac` driver is aligned with what we need. This PR is the minimal infrastructure to add the new drivers. In particular, it does the following: 1. Adds two new cli drivers, `juliax` and `juliac`. At the moment, `juliac` is a placeholder and just errors, while `juliax` behaves the same as `julia` (except to error on the deprecated `--math-mode=fast`, just as an example of a behavior difference). 2. Documents that the behavior of `julia` (but not `juliax` or `juliac`) is pat of the julia public API. 3. Switches the cli mode based on the argv[0] of the binary. I.e. all three binaries are identical, except for their name, the same way that, e.g. `clang` and `clang++` are the same binary just with different names. On Unix systems, these are symlinks. On windows, separate copies of the same (small) binary. There is a fallback cli option `--cli-mode` that can be used in case the argv[0] detection is not available (e.g. for some fringe embedded use cases). 4. There is currently no separate `-debug` version of the new drivres. My intention is to make this dependent on the ordinary debug flags, rather than having a separate driver. Once this is merged, I intend to resubmit #50974 (chaning `juliax` only), and then finish and hook up `juliac` shortly thereafter.
What are some other examples, and more importantly, can I propose some more, since "experimental" anyway? It seems juliax is meant for scripting and/or is experimental (for now), so I suggest --startup-file=no as its default, at a minimum. I was shot down previously on that new default (even for non-interactive). Since it's experimental, not committing to an API(?), does that imply 2.0, or allowing some such? I then suggest a radically smaller sysimage for it, e.g. Pkg out right away (I've done it, it's not difficult if you just want to disable it), and then julia implies juliax plus doing:
If this is ok, then I would suggest excising LinearAlgebra too (and OpenBLAS, neither very useful for scripting, both recovered with
If this is meant for scripting, then the separate console and non-console apps are encoded into the .exe, so they can't be the same, thus there is at least that need for two for Julia source code, as opposed to compiled. [Python has a separate .pyw ending available for "Windows console" apps. I.e. Windows needs two types of apps, and thus two drivers. It's unclear to me if Julia needs a new ending or just such a driver, or maybe if PackageCompiler.jl covers this. And now juliac, it that its future replacement?]
Great future plan, does that mean you would potentially download compiled pkgimages for packages from the Julia registry, not just source code not precompiled? Typos "pat of the julia public API", "drivres", "chaning".
I'm confused, is there any need to error? Rather just silently ignore? Or both could allow on and off, just with different defaults? |
I'm a bit sad to see this being bundled into a new CLI driver instead of an entrypoint marker or something else where the meaning is clear in the code itself. |
In addition to the entrypoint point (which I agree with), I have two additional things: Does this prevent a REPL-then-deploy style workflow? I think I mentioned it elsewhere, but I'm currently able to compile a binary for AVR/microcontrollers from the REPL, change a thing in my source code, reload through Revise, inspect existing IR, and only have to (pre)compile the new addition, keeping compilation extremely snappy. That's something traditional AoT compiled languages had to fight hard for to get from their toolchains, and is a workflow I'd very much like to keep. So even with a seperate driver, I doubt I'd use it (except for CI deployment, I guess?).
What does this mean? Which "julia" is this referring to, if that is supposed to "go away" in the future (be replaced by |
At present there are none. Yes, you may suggest additional behavior, but please not in this issue. The CLI is expected to be unstable for multiple releases. |
There's no instead here. Entrypoints in projects, multiple entrypoints, complex deployment setups are all build system concerns (which for us is |
The current |
@@ -42,6 +42,9 @@ You can get a complete list of the public symbols from a module with `names(MyMo | |||
|
|||
Package authors are encouraged to define their public API similarly. | |||
|
|||
In addition, the documented and semantically observable behaviors of the `julia` CLI driver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's okay to say that the documented behaviors of the julia
driver are public API, since that's consistent with the rest of this FAQ entry (which says that documented behaviors of public symbols are public API). But "semantically observable behaviors" seems too vague and hard to define.
In addition, the documented and semantically observable behaviors of the `julia` CLI driver | |
In addition, the documented behaviors of the `julia` CLI driver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would have prevented the --math-mode=fast
removal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm misunderstanding what you've written here. Which of the following interpretations is correct?
Interpretation 1:
- The documented behaviors of the
julia
driver are public API. - The semantically observable behaviors of the
julia
driver are public API.
Interpretation 2:
- A behavior of the
julia
driver is public API iff the behavior is documented and the behavior is semantically observable.
When I read the above text, I interpreted it as interpretation 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interpretation 2 is what I intended. In particular options that are non-semantic (optimization levels, fastmath, etc.) are not guaranteed to work the same across versions, even if the documentaiton tells you what they do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Interpretation 2 sounds good to me.
Let's reword the sentence to make it more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about something along the lines of:
A behavior of the
julia
CLI driver is only part of the public API if the behavior is documented and the behavior is semantically observable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it might also be helpful to add some form of the sentence that you wrote above, since I think it gives some good illustrative examples:
For example, options that are non-semantic (optimization levels, fastmath, etc.) are not guaranteed to work the same across versions, even if the documentation tells you what they do.
It feels very strange to me to have two subtly different executables that mostly do the same thing. A key point in #50974 was that the name of the entry point should be easy-to-use and obvious. I don't want to be telling people that the feature they want is tucked away in an "experimental" executable that doesn't have stability guarantees. This seems to be throwing out the baby with the bathwater. |
Well, this gives is freedom to diverge, but yeah, obviously this it not perfect, but there were enough people angry enough about the prospect of changing the behavior of |
I guess this just seems like a rather extreme approach, and as far as I can tell from reading the public conversations, it seems to be motivated by a purely aesthetic desire to write function main(ARGS)
...
end without any other signals that |
is having a kinda-new-kinda-fragile-slightly-different-but-mostly-the-same-but-cant-remember-how now I must have two sets of code because I must be afraid that users won't know whether the code I delivered should be run with |
Sorry if this is a dumb or already-answered question, but what's the medium/long-term plan for I think I agree w/ @mbauman and @MasonProtter here that it seems a bit excessive. I get Just as an FYI, I'm probably the only person out there that |
The uproar was nearly entirely over breakage with I know you didn't like
|
That sounds a bit reductive to me, but it's not wrong exactly. I played with a bunch of options, and that's the one that I thought worked best. Of course it's possible that I'm wrong and the design is dumb, but hey with this at least we can delete the cli driver again in that case.
I don't know about aesthetic, but several people objected to changing the behavior at all, even if the name is
Well kind of, but some of the options would be considered to breaking to change as defaults in the |
I worry this will bifurcate / fragment the ecosystem yet more if you have some code that only works with And there must be such code, because otherwise if everything behaves the same with each driver, no point in separating! |
Yes, the macro options were discussed, but people didn't like them, because it wasn't clear how the scoping and export of them would work. Also, there's scenarios where the However, I want to re-emphasize again that the main thing isn't the only thing that the new cli driver is for. There's other changes I want to make to the behavior of the cli driver - the |
I think this is a valid concern. I would be fine with disallowing |
Ok, I could be convinced. I may be missing the forest for a single |
Lots of people in this and previous PR seemed very amicable to the entrypoint idea, which would solve that problem handily without requiring bifurcation of the CLI. Maybe the discussions you're referencing happened elsewhere, in which case it would be great if you could share them in more detail.
This sounds like there are a lot of changes coming that rely on your exact version/implementation? It would be great if you could share those plans with the community before dismissing the (imo valid) concerns people have about the current approach. Otherwise it feels like you're trying to force us to accept your vision, which admittedly doesn't feel nice.
That also sounds like something an explicit entrypoint declaration would handle amicably - I don't think there were big arguments against it in the previous PR, and lots of people 👍 comments mentioning it. |
I'm gonna ignore this, but as I've said before, I really don't like this kind of comment, and I would again ask you to please refrain from it. It's just so unnecessarily confrontational. There's no sides here. We're all trying to work together to find a good design here.
I don't have any concrete plans for additional changes, but there have been various such changes discussed over the years that we might want to do. I mentioned two of them in the original post, but there may be others.
There was some discussion about it on slack, some discussion on the triage call, and I also talked to @vtjnash about it in person after he first proposed it, but the summary is basically the following. There's two ways that this could work
The first is undesirable, because it's unclear how it should work with entrypoints that are not generic functions, and the import/export of the special internal symbol is weird. Obviously there can be syntax for it, but that's like 3 or 4 different special cases that need to be learned just to avoid using the name that everybody actually wants to call it. The second option isn't great, because it's not clear what to do with conflicting entrypoint definitions and it again needs special syntax to support entrypoints that are not generic functions. It also doesn't support any form of entrypoint import/export or the interactive version, particularly in a world where the manifest is autoloaded. |
If we change our mind in the future, we can just straight up remove So it seems that this PR doesn't lock us into anything that we can't change or get rid of in the future. And if it's going to be helpful to Keno and others as far as unblocking other work they want to do, I think it makes sense to move forward with this PR. |
And the design discussions around the entrypoint macro ( |
a clarifying question for my own benefit: is this intended to be
or
I suspect this is one of those situations where the tone/intentions of a proposal can have a big impact on its reception, as it appears to me that much of the concern is uncertainty about the medium-long term vision |
Two cents from me:
|
I'm not trying to be confrontational here (though I'm happy to chat on slack/VC if there's a particular issue with me expressing my concerns here); I'm trying to figure out why a split of The reason I personally dislike the split is because I worry that this causes static compilation (and hence cross compilation) to be segregated away from
Ok, so (please correct me if I'm wrong) I'm interpreting this as "it just seems like something neat, but there are no concrete reasons this split needs to be done in order to achieve the changes/things we wished for in the past". So I guess that leaves the big question - what does this split, on its own merits, allow us to do, that couldn't be done with a regular API hook or commandline switch?
I don't follow. What is an entry point that is not a generic function? The entry point is special, and hence should be a special thing that ought to be learned. Julia currently doesn't have entry points as a concept, and so we're free to define what it means to be an entry point - there's no requirement I'm aware of that means only generic functions can be entry points. Making entry points special, e.g. via
I think this is conflating multiple different modes of execution unnecessarily. I don't think there's an expectation of compiling standalone scripts into a statically compilable & runnable executable. Conflicting entrypoint definitions should error, at least in a first iteration. Multi-entrypoint support can be easily built on top of it, by providing a singular multi-entrypoint entrypoint. Not sure what you mean with entrypoint import/export, can you clarify what that is? What is an interactive version of an entrypoint? Again, I'm just trying to understand what the goal here is. There seem to be a lot of distinct topics that get mixed up and end up in a very confusing ball of yarn.
I mean, as with lots of things in Base.Experimental, people will start to rely on it being there, locking us in. I'd love to know what the plan for this future work is though, that requires this kind of seperation 🤷 |
Since this issue seems to be reviving the |
Yes, that's the point of doing it this way.
Yes, if somebody wants to do the work, and show that it's better for some reason that wasn't considered and put up a subsequent PR that's always an option of course.
Probably this one, but it's uncertain and depends on how the static compilation work goes, whether it's good enough that most scripts will default to
This is basically that
Yes, this is part of the design objectives for
The compilation part is in
The concrete reason is there is significant uncertainty about the correct design direction and significant opposition to doing such experiments with the main As for the other changes that might be made, there are no concrete designs, but I will defer again to the two examples in the original proposal that we wanted to do, but couldn't because of concerns about CLI compatibility
None of these have concrete designs, but that's precisely why having a driver that's experimental and unstable for a few releases is useful to prototype these things. If doing language design for the past decade has taught me anything is that the way you get to a good solution is that you start with an ok one, implement it, see how it feels and then iterate from there. The only issue here is that reliance issues now restrict the way in which we can do these experiments, so we need to come up with some creative solutions to enable this experimentation. I will also point out that none of these discussions are new. We've been having them for the better part of the past decade in various fora (e.g. see #15864). I've been doing my best to summarize, but there's just a lot of context and previous discussion.
Generic functions in
as an API. Whether or not that's the right API is TBD, but it should at least be possible.
Requiring a Project.toml just to build a binary is a non-starter, as was mentioned multiple times. The issues with the macro I discussed above.
That's like saying that nobody will use
The
Well, I think it'd be a lot clearer if people would just let me finish implementing it, so that they can actually try it out but here we are ;).
We don't make design decisions by majority vote. That's how you end up with UTF16. The way this works is that everybody contributes ideas, we think about the technical pro/cons, collect technical objections, solutions, etc. And, yes, to some extent it's up to the technical and aesthetic discretion of the person putting in the work. Even triage is just a fast-pathed version of this. If something is really controversial with many committers disagreeing, it sometimes goes into small discussion, but I don't really think we're there yet. |
I don't know (all of Keno's) goals. But only one julia executable can't work, if you want to support the two types of Windows apps. So I at least welcome this new infrastructure. |
Apologies because I don't want to drag this out and cause more strife, but I'm still having trouble understanding the reasoning against an entrypoint/main macro.
Couldn't this just be written as something like const main = @entrypoint enforce_static(:runtime => false) do ARGS
println("Hello World")
end or const main = enforce_static(:runtime => false) do ARGS
println("Hello World")
end
@entrypoint main
and similarly couldn't this be module MyApp
@entrypoint function main(ARGS)
...
end
export main
end ? I promise I'll drop it after this as I can tell I'm probably not helping, and likely just bogging things down, but I just want to understand if there's a technical barrier here I'm not understanding. |
Do you have concrete semantics in mind? Obviously that could be written, but what does it mean ;)? I think as a new rule anybody that proposes a new macro needs to tell me what the macro expands to ;). This is not meant as a dismissal, it's just really hard to be precise in my thoughts if I don't know exactly what the proposal is. In the |
Sure, that's a good rule. What I'm picturing here is that in Line 536 in cc4108b
if isdefined(Main, :main) && !is_interactive
if Core.Compiler.generating_sysimg()
precompile(Main.main, (typeof(ARGS),))
else
ret = invokelatest(Main.main, ARGS)
end we'd write something more like the_entrypoint = Core.maybe_get_main_entrypoint()
if !isnothing(the_entrypoint) && !is_interactive
if Core.Compiler.generating_sysimg()
precompile(the_entrypoint, (typeof(ARGS),))
else
ret = invokelatest(the_entrypoint, ARGS)
end and then would have two functions Core.register_entrypoint!
Core.maybe_get_entrypoint which would (in a hopefully type stable way(?)) be able to store and retrieve an object to be called as an entrypoint. Then the macro would expand from @entrypoint enforce_static(:runtime => false) do ARGS
println("Hello World")
end to something like begin
local _entrypoint = enforce_static(:runtime => false) do ARGS
println("Hello World")
end
Core.register_entrypoint!(_entrypoint)
_entrypoint
end I know that's not the most complete possible specification, but hopefully it at least shows what I was picturing. |
Right, that's the "magic side effect" option that was proposed in various forms. The general objection to that was that we don't necessarily want merely loading a package that defines an entrypoint to automatically cause its execution. There's a couple of different options for semantics that I think work better, but I'm still skeptical of.
I think this would work for the
I don't think this has been proposed so far in this form, but I think of all the possible options, that's the probably the best alternative so far. The primary issues I see with this are:
Still I think that variant might work out ok. I'll mock it up and see if it works. |
The above is probably somewhat more convoluted than it needs to be with the function stubs and whatnot. Even simpler would just be something like if isdefined(Base, :main_entrypoint) && !is_interactive
if Core.Compiler.generating_sysimg()
precompile(Base.main_entrypoint, (typeof(ARGS),))
else
ret = invokelatest(Base.main_entrypoint, ARGS)
end for @entrypoint ex macroexpand to begin
const Base.main_entrypoint = ex
end |
I see, thank you for laying out those thoughts.
Yeah that's definitely a real concern. Though I think for somewhat similar (but maybe more restricted reasons), we might also not want a package That is, it's probably best for the marking to happen in the
that's definitely an interesting idea. I think maybe to some intermediate users it might be a bit confusing because it's exotic, but I think for the majority of users they shouldn't have too much trouble understanding that writing E.g. analogously someone might ask "okay, but how exactly does |
As they say, if at first you don't succeed, try again, then try again, add an extra layer of indirection and take a little bit of spice from every other idea and you've got yourself a wedding cake. Or something like that, I don't know - at times it felt like this cake was getting a bit burnt. Where was I? Ah yes. This is the third edition of the main saga (#50974, #51417). In this version, the spelling that we'd expect for the main use case is: ``` function (@main)(ARGS) println("Hello World") end ``` This syntax was originally proposed by `@vtjnash`. However, the semantics here are slightly different. `@main` simply expands to `main`, so the above is equivalent to: ``` function main(ARGS) println("Hello World") end @main ``` So `@main` is simply a marker that the `main` binding has special behavior. This way, all the niceceties of import/export, etc. can still be used as in the original `Main.main` proposal, but there is an explicit opt-in and feature detect macro to avoid executing this when people do not expect. Additionally, there is a smooth upgrade path if we decide to automatically enable `Main.main` in Julia 2.0.
As they say, if at first you don't succeed, try again, then try again, add an extra layer of indirection and take a little bit of spice from every other idea and you've got yourself a wedding cake. Or something like that, I don't know - at times it felt like this cake was getting a bit burnt. Where was I? Ah yes. This is the third edition of the main saga (#50974, #51417). In this version, the spelling that we'd expect for the main use case is: ``` function (@main)(ARGS) println("Hello World") end ``` This syntax was originally proposed by `@vtjnash`. However, the semantics here are slightly different. `@main` simply expands to `main`, so the above is equivalent to: ``` function main(ARGS) println("Hello World") end @main ``` So `@main` is simply a marker that the `main` binding has special behavior. This way, all the niceceties of import/export, etc. can still be used as in the original `Main.main` proposal, but there is an explicit opt-in and feature detect macro to avoid executing this when people do not expect. Additionally, there is a smooth upgrade path if we decide to automatically enable `Main.main` in Julia 2.0.
#51435 for the potluck variant. |
As they say, if at first you don't succeed, try again, then try again, add an extra layer of indirection and take a little bit of spice from every other idea and you've got yourself a wedding cake. Or something like that, I don't know - at times it felt like this cake was getting a bit burnt. Where was I? Ah yes. This is the third edition of the main saga (#50974, #51417). In this version, the spelling that we'd expect for the main use case is: ``` function (@main)(ARGS) println("Hello World") end ``` This syntax was originally proposed by `@vtjnash`. However, the semantics here are slightly different. `@main` simply expands to `main`, so the above is equivalent to: ``` function main(ARGS) println("Hello World") end @main ``` So `@main` is simply a marker that the `main` binding has special behavior. This way, all the niceceties of import/export, etc. can still be used as in the original `Main.main` proposal, but there is an explicit opt-in and feature detect macro to avoid executing this when people do not expect. Additionally, there is a smooth upgrade path if we decide to automatically enable `Main.main` in Julia 2.0.
As they say, if at first you don't succeed, try again, then try again, add an extra layer of indirection and take a little bit of spice from every other idea and you've got yourself a wedding cake. Or something like that, I don't know - at times it felt like this cake was getting a bit burnt. Where was I? Ah yes. This is the third edition of the main saga (#50974, #51417). In this version, the spelling that we'd expect for the main use case is: ``` function (@main)(ARGS) println("Hello World") end ``` This syntax was originally proposed by `@vtjnash`. However, the semantics here are slightly different. `@main` simply expands to `main`, so the above is equivalent to: ``` function main(ARGS) println("Hello World") end @main ``` So `@main` is simply a marker that the `main` binding has special behavior. This way, all the niceceties of import/export, etc. can still be used as in the original `Main.main` proposal, but there is an explicit opt-in and feature detect macro to avoid executing this when people do not expect. Additionally, there is a smooth upgrade path if we decide to automatically enable `Main.main` in Julia 2.0.
Following the discussion in #50974, it became clear that there is significant objection to changes to the behavior of the julia CLI driver. Some commenters found changes to the behavior acceptable if they were unlikely to impact existing code (e.g. in the case of #50974 using
__main__
instead ofmain
), while other were worried about the reputational risks of even changing behavior in the corner case. In subsequent discussion it became relatively clear that the only way forward that did not raise significant objection was to introduce a new CLI driver for the new behavior. This may seem a bit drastic just for the change in #50974, but I actually think there's a number of other quality-of-life improvements that this would enable us to do over time, for example:In addition, it doesn't seem so bad to have some CLI design flexibility to make sure that the
juliac
driver is aligned with what we need.This PR is the minimal infrastructure to add the new drivers. In particular, it does the following:
Adds two new cli drivers,
juliax
andjuliac
. At the moment,juliac
is a placeholder and just errors, whilejuliax
behaves the same asjulia
(except to error on the deprecated--math-mode=fast
, just as an example of a behavior difference).Documents that the behavior of
julia
(but notjuliax
orjuliac
) is pat of the julia public API.Switches the cli mode based on the argv[0] of the binary. I.e. all three binaries are identical, except for their name, the same way that, e.g.
clang
andclang++
are the same binary just with different names. On Unix systems, these are symlinks. On windows, separate copies of the same (small) binary. There is a fallback cli option--cli-mode
that can be used in case the argv[0] detection is not available (e.g. for some fringe embedded use cases).There is currently no separate
-debug
version of the new drivres. My intention is to make this dependent on the ordinary debug flags, rather than having a separate driver.Once this is merged, I intend to resubmit #50974 (chaning
juliax
only), and then finish and hook upjuliac
shortly thereafter.