Skip to content

Commit

Permalink
Standardize the entry-point for Julia execution (#50974)
Browse files Browse the repository at this point in the history
This is a bit of a straw-man proposal (though I think mergable if people
agree) to standardize the execution entrypoint for Julia scripts. I
think there's at least four different ways that people might run a
script:

- As `julia main.jl`
- As a PkgCompiler sysimage, then calling the main entry point
- As a PkgCompiler "app", with the magic `julia_main` function
- As a StaticCompiler product with an explicit entrypoint specified on
the API.

The main problem I have with all of these variants is that they're all
different and it's kind of a pain to move between them. Here I propose
that we standardize on `Main.main(ARGS)` as the entrypoint for all
scripts. Downstream from that proposal, this PR then makes the following
changes:

1. If a system image has an existing `Main.main`, that is the entry
point for `julia -Jsysimage.so`.
2. If not, and the sysimage has a REPL, we call REPL.main (we could
handle this by defaulting `Main.main` to a weak import of `REPL.main`,
but for the purpose of this PR, it's an explicit fallback. That said, I
do want to emhpasize the direction of moving the REPL to be "just
another app".
3. If the REPL code is called and passed a script file, the REPL
executes any newly defined Main.main after loading the script file. As a
result, `julia` behaves the same as if we had generated a new system
image after loading `main.jl` and then running julia with that system
image.

The further downstream implication of this is that I'd like to get rid
of the distinction between PkgCompiler apps and system images. An app is
simply a system image with a `Main.main` function defined (note that
currently PkgCompiler uses `julia_main` instead).

---------

Co-authored-by: Martijn Visser <[email protected]>
  • Loading branch information
Keno and visr authored Sep 3, 2023
1 parent 0cc0dbd commit 8e14322
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 17 deletions.
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Compiler/Runtime improvements
Command-line option changes
---------------------------

* The entry point for Julia has been standardized to `Main.main(ARGS)`. When julia is invoked to run a script or expression
(i.e. using `julia script.jl` or `julia -e expr`), julia will subsequently run the `Main.main` function automatically if
such a function has been defined. This is intended to unify script and compilation workflows, where code loading may happen
in the compiler and execution of `Main.main` may happen in the resulting executable. For interactive use, there is no semantic
difference between defining a `main` function and executing the code directly at the end of the script. ([50974])

Multi-threading changes
-----------------------

Expand Down
35 changes: 20 additions & 15 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,8 @@ incomplete_tag(exc::Meta.ParseError) = incomplete_tag(exc.detail)

cmd_suppresses_program(cmd) = cmd in ('e', 'E')
function exec_options(opts)
quiet = (opts.quiet != 0)
startup = (opts.startupfile != 2)
history_file = (opts.historyfile != 0)
color_set = (opts.color != 0) # --color!=auto
global have_color = color_set ? (opts.color == 1) : nothing # --color=on
global have_color = (opts.color != 0) ? (opts.color == 1) : nothing # --color=on
global is_interactive = (opts.isinteractive != 0)

# pre-process command line argument list
Expand Down Expand Up @@ -323,15 +320,8 @@ function exec_options(opts)
end
end
end
if repl || is_interactive::Bool
b = opts.banner
auto = b == -1
banner = b == 0 || (auto && !interactiveinput) ? :no :
b == 1 || (auto && interactiveinput) ? :yes :
:short # b == 2
run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
end
nothing

return repl
end

function _global_julia_startup_file()
Expand Down Expand Up @@ -548,13 +538,28 @@ function _start()
append!(ARGS, Core.ARGS)
# clear any postoutput hooks that were saved in the sysimage
empty!(Base.postoutput_hooks)
local ret = 0
try
exec_options(JLOptions())
repl_was_requested = exec_options(JLOptions())
if isdefined(Main, :main) && !is_interactive
if Core.Compiler.generating_sysimg()
precompile(Main.main, (typeof(ARGS),))
else
ret = invokelatest(Main.main, ARGS)
end
elseif (repl_was_requested || is_interactive)
if isassigned(REPL_MODULE_REF)
ret = REPL_MODULE_REF[].main(ARGS)
end
end
ret === nothing && (ret = 0)
ret = Cint(ret)
catch
ret = Cint(1)
invokelatest(display_error, scrub_repl_backtrace(current_exceptions()))
exit(1)
end
if is_interactive && get(stdout, :color, false)
print(color_normal)
end
return ret
end
52 changes: 52 additions & 0 deletions doc/src/manual/command-line-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,58 @@ $ julia --color=yes -O -- script.jl arg1 arg2..

See also [Scripting](@ref man-scripting) for more information on writing Julia scripts.

## The `Main.main` entry point

At the conclusion of executing a script or expression, `julia` will attempt to execute the function
`Main.main(ARGS)` (if such a function has been defined). This feature is intended to aid in the unification
of compiled and interactive workflows. In compiled workflows, loading the code that defines the `main`
function may be spatially and temporally separated from the invocation. However, for interactive workflows,
the behavior is equivalent to explicitly calling `exit(main(ARGS))` at the end of the evaluated script or
expression.

!!! compat "Julia 1.11"
The special entry point `Main.main` was added in Julia 1.11. For compatibility with prior julia versions,
add an explicit `VERSION < v"1.11" && exit(main(ARGS))` at the end of your scripts.

To see this feature in action, consider the following definition, which will execute the print function despite there being no explicit call to `main`:

```
$ julia -e 'main(ARGS) = println("Hello World!")'
Hello World!
$
```

Only the `main` binding in the `Main`, module has this special behavior. For example, using `hello`
instead of `main` will result in the `hello` function not executing:

```
$ julia -e 'hello(ARGS) = println("Hello World!")'
$
```

The `main` binding may be imported from a package. A hello package defined as

```
module Hello
export main
main(ARGS) = println("Hello from the package!")
end
```

may be used as:

```
$ julia -e 'using Hello'
Hello from the package!
$ julia -e 'import Hello' # N.B.: Execution depends on the binding not whether the package is loaded
$
```

However, note that the current best practice recommendation is to not mix application and reusable library
code in the same package. Helper applications may be distributed as separate pacakges or as scripts with
separate `main` entry points in a package's `bin` folder.

## Parallel mode

Expand Down
8 changes: 6 additions & 2 deletions src/jlapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,20 @@ static NOINLINE int true_main(int argc, char *argv[])

if (start_client) {
jl_task_t *ct = jl_current_task;
int ret = 1;
JL_TRY {
size_t last_age = ct->world_age;
ct->world_age = jl_get_world_counter();
jl_apply(&start_client, 1);
jl_value_t *r = jl_apply(&start_client, 1);
if (jl_typeof(r) != (jl_value_t*)jl_int32_type)
jl_type_error("typeassert", (jl_value_t*)jl_int32_type, r);
ret = jl_unbox_int32(r);
ct->world_age = last_age;
}
JL_CATCH {
jl_no_exc_handler(jl_current_exception(), ct);
}
return 0;
return ret;
}

// run program if specified, otherwise enter REPL
Expand Down
16 changes: 16 additions & 0 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1506,4 +1506,20 @@ end

import .Numbered.numbered_prompt!

# TODO: Move more of this implementation into REPL.
function main(ARGS)
opts = Base.JLOptions()
interactiveinput = isa(stdin, Base.TTY)
b = opts.banner
auto = b == -1
banner = b == 0 || (auto && !interactiveinput) ? :no :
b == 1 || (auto && interactiveinput) ? :yes :
:short # b == 2

quiet = (opts.quiet != 0)
history_file = (opts.historyfile != 0)
color_set = (opts.color != 0) # --color!=auto
Base.run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
end

end # module
12 changes: 12 additions & 0 deletions test/cmdlineargs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -977,3 +977,15 @@ end
#heap-size-hint, we reserve 250 MB for non GC memory (llvm, etc.)
@test readchomp(`$(Base.julia_cmd()) --startup-file=no --heap-size-hint=500M -e "println(@ccall jl_gc_get_max_memory()::UInt64)"`) == "$((500-250)*1024*1024)"
end

## `Main.main` entrypoint

# Basic usage
@test readchomp(`$(Base.julia_cmd()) -e 'main(ARGS) = println("hello")'`) == "hello"

# Test ARGS with -e
@test readchomp(`$(Base.julia_cmd()) -e 'main(ARGS) = println(ARGS)' a b`) == repr(["a", "b"])

# Test import from module
@test readchomp(`$(Base.julia_cmd()) -e 'module Hello; export main; main(ARGS) = println("hello"); end; using .Hello'`) == "hello"
@test readchomp(`$(Base.julia_cmd()) -e 'module Hello; export main; main(ARGS) = println("hello"); end; import .Hello'`) == ""

4 comments on commit 8e14322

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily package evaluation, I will reply here when finished:

@nanosoldier runtests(isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

Please sign in to comment.