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

Main entrypoint take 3 - revenge of the macro #51435

Merged
merged 1 commit into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Main entrypoint take 3 - revenge of the macro
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.
  • Loading branch information
Keno committed Oct 8, 2023
commit 7847dcc78a06fca9728db8d46084567b0cfcf621
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Compiler/Runtime improvements
Command-line option changes
---------------------------

* The entry point for Julia has been standardized to `Main.main(ARGS)`. This must be explicitly opted into using the `@main` macro
(see the docstring for futher details). When opted-in, and 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.
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
125 changes: 110 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 @@ -536,6 +526,13 @@ definition of `eval`, which evaluates expressions in that module.
"""
MainInclude.eval

function should_use_main_entrypoint()
isdefined(Main, :main) || return false
M_binding_owner = Base.binding_module(Main, :main)
(isdefined(M_binding_owner, Symbol("#__main_is_entrypoint__#")) && M_binding_owner.var"#__main_is_entrypoint__#") || return false
return true
end

"""
include([mapexpr::Function,] path::AbstractString)

Expand Down Expand Up @@ -565,13 +562,111 @@ 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 should_use_main_entrypoint() && !is_interactive
if Core.Compiler.generating_output()
precompile(Main.main, (typeof(ARGS),))
else
ret = invokelatest(Main.main, ARGS)
end
elseif (repl_was_requested || is_interactive)
# Run the Base `main`, which will either load the REPL stdlib
# or run the fallback REPL
ret = repl_main(ARGS)
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

function repl_main(_)
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
return run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
end

"""
@main

This macro is used to mark that the binding `main` in the current module is considered an
entrypoint. The precise semantics of the entrypoint depend on the CLI driver.

In the `julia` driver, if `Main.main` is marked as an entrypoint, it will be automatically called upon
the completion of script execution.

The `@main` macro may be used standalone or as part of the function definition, though in the latter
case, parenthese are required. In particular, the following are equivalent:

```
function (@main)(ARGS)
println("Hello World")
end
```

```
function main(ARGS)
end
@main
```

## Detailed semantics

The entrypoint semantics attach to the owner of the binding owner. In particular, if a marked entrypoint is
imported into `Main`, it will be treated as an entrypoint in `Main`:

```
module MyApp
export main
(@main)(ARGS) = println("Hello World")
end
using .MyApp
# `julia` Will execute MyApp.main at the conclusion of script execution
```

Note that in particular, the semantics do not attach to the method
or the name:
```
module MyApp
(@main)(ARGS) = println("Hello World")
end
const main = MyApp.main
# `julia` Will *NOT* execute MyApp.main unless there is a separate `@main` annotation in `Main`

!!! compat "Julia 1.11"
This macro is new in Julia 1.11. At present, the precise semantics of `@main` are still subject to change.
```
"""
macro main(args...)
if !isempty(args)
error("USAGE: `@main` is expected to be used as `(@main)` without macro arguments.")
end
if isdefined(__module__, :main)
Copy link
Sponsor Member

@vtjnash vtjnash Sep 28, 2023

Choose a reason for hiding this comment

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

Ideally we could return this code as

Expr(:toplevel_but_last,
   quote everything end,
   esc(:main) )

instead of running it, so as to avoid having the macro itself do side-effects and throw error

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know if lowering is really set up to do this, as it'd have to be able to hoist that toplevel expression. @JeffBezanson ? But yes, if possible that would be better of course.

if Base.binding_module(__module__, :main) !== __module__
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module.")
Comment on lines +662 to +663
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should allow @main to be also used in Main. i.e. something like

Suggested change
if Base.binding_module(__module__, :main) !== __module__
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module.")
if Base.binding_module(__module__, :main) !== __module__ || __module__ === Main
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module, or in Main.")

That way someone can alternatively do

module MyApp

export main
main(ARGS) = println("Hello World")

end
using .MyApp
@main 

if they want. This is arguably a bit more explicit and clear, since it kinda makes me nervous to using a module and have that silently define an entrypoint.

I guess though that the downside of this would be that it wouldn't map nicely onto a hypotheical 2.0 world where @main just becomes unadorned main.

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered it, but I'd prefer not to for the moment. You can always do const (@main) = MyApp.main for the above case.

end
end
Core.eval(__module__, quote
# Force the binding to resolve to this module
global main
global var"#__main_is_entrypoint__#"::Bool = true
end)
esc(:main)
end
4 changes: 3 additions & 1 deletion base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,9 @@ export
@goto,
@view,
@views,
@static
@static,

@main

# TODO: use normal syntax once JuliaSyntax.jl becomes available at this point in bootstrapping
eval(Expr(:public,
Expand Down
67 changes: 67 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,73 @@ $ 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

As of Julia, 1.11, Base export a special macro `@main`. This macro simply expands to the symbol `main`,
but 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 and this behavior was opted into
using the `@main macro`. 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 `@isdefined(var"@main") ? (@main) : 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 and only if
the macro `@main` was used within the defining module.

For example, using `hello` instead of `main` will result not result in the `hello` function executing:

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

and neither will a plain definition of `main`:
```
$ julia -e 'main(ARGS) = println("Hello World!")'
$
```

However, the opt-in need not occur at definition time:
$ julia -e 'main(ARGS) = println("Hello World!"); @main'
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
12 changes: 12 additions & 0 deletions test/cmdlineargs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -982,3 +982,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'`) == ""