Skip to content
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

Standardize the entry-point for Julia execution #50974

Merged
merged 6 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 identended to unify script and compilation workflows, where code loading may happen
Keno marked this conversation as resolved.
Show resolved Hide resolved
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
34 changes: 31 additions & 3 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ function exec_options(opts)
end
end
end

ret = 0
if isdefined(Main, :main)
Copy link
Sponsor Member

@vtjnash vtjnash Aug 31, 2023

Choose a reason for hiding this comment

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

Suggested change
if isdefined(Main, :main)
if (repl || arg_is_program) && isdefined(Main, :main)

?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
if isdefined(Main, :main)
if !isinteractive && isdefined(Main, :main)

suggestion from triage

if Core.Compiler.generating_sysimg()
precompile(Main.main, (typeof(ARGS),))
else
ret = invokelatest(Main.main, ARGS)
end
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

actually, maybe just this:

Suggested change
end
end
repl = false

end
vtjnash marked this conversation as resolved.
Show resolved Hide resolved

if repl || is_interactive::Bool
b = opts.banner
auto = b == -1
Expand All @@ -331,7 +341,7 @@ function exec_options(opts)
:short # b == 2
run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
end
nothing
return ret
end

function _global_julia_startup_file()
Expand Down Expand Up @@ -548,13 +558,31 @@ function _start()
append!(ARGS, Core.ARGS)
# clear any postoutput hooks that were saved in the sysimage
empty!(Base.postoutput_hooks)
local ret
try
exec_options(JLOptions())
if isdefined(Core, :Main) && isdefined(Core.Main, :main)
ret = Core.Main.main(ARGS)
elseif isassigned(REPL_MODULE_REF)
ret = REPL_MODULE_REF[].main(ARGS)
else
# TODO: This is the case for system image execution without main function
# and without REPL loaded. We fall back here to the pre-main behavior.
# However, we may instead want to adjust the sysimage build process to
# emit an explicit main function for this case. We should revisit this once
# the `main` story is fully worked out.
#
# error("No entry point defined and REPL not loaded.")
#
ret = exec_options(JLOptions())
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.
JeffBezanson marked this conversation as resolved.
Show resolved Hide resolved

## 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
3 changes: 3 additions & 0 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1506,4 +1506,7 @@ end

import .Numbered.numbered_prompt!

# TODO: Move more of this implementation into REPL.
main(ARGS) = Base.exec_options(Base.JLOptions())

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'`) == ""