Skip to content

Commit f90fd72

Browse files
authored
Main entrypoint take 3 - revenge of the macro (#51435)
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.
1 parent e81c8e3 commit f90fd72

File tree

6 files changed

+205
-18
lines changed

6 files changed

+205
-18
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ Compiler/Runtime improvements
2121
Command-line option changes
2222
---------------------------
2323

24+
* The entry point for Julia has been standardized to `Main.main(ARGS)`. This must be explicitly opted into using the `@main` macro
25+
(see the docstring for futher details). When opted-in, and julia is invoked to run a script or expression
26+
(i.e. using `julia script.jl` or `julia -e expr`), julia will subsequently run the `Main.main` function automatically.
27+
This is intended to unify script and compilation workflows, where code loading may happen
28+
in the compiler and execution of `Main.main` may happen in the resulting executable. For interactive use, there is no semantic
29+
difference between defining a `main` function and executing the code directly at the end of the script. ([50974])
30+
2431
Multi-threading changes
2532
-----------------------
2633

base/client.jl

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,8 @@ incomplete_tag(exc::Meta.ParseError) = incomplete_tag(exc.detail)
228228

229229
cmd_suppresses_program(cmd) = cmd in ('e', 'E')
230230
function exec_options(opts)
231-
quiet = (opts.quiet != 0)
232231
startup = (opts.startupfile != 2)
233-
history_file = (opts.historyfile != 0)
234-
color_set = (opts.color != 0) # --color!=auto
235-
global have_color = color_set ? (opts.color == 1) : nothing # --color=on
232+
global have_color = (opts.color != 0) ? (opts.color == 1) : nothing # --color=on
236233
global is_interactive = (opts.isinteractive != 0)
237234

238235
# pre-process command line argument list
@@ -323,15 +320,8 @@ function exec_options(opts)
323320
end
324321
end
325322
end
326-
if repl || is_interactive::Bool
327-
b = opts.banner
328-
auto = b == -1
329-
banner = b == 0 || (auto && !interactiveinput) ? :no :
330-
b == 1 || (auto && interactiveinput) ? :yes :
331-
:short # b == 2
332-
run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
333-
end
334-
nothing
323+
324+
return repl
335325
end
336326

337327
function _global_julia_startup_file()
@@ -536,6 +526,13 @@ definition of `eval`, which evaluates expressions in that module.
536526
"""
537527
MainInclude.eval
538528

529+
function should_use_main_entrypoint()
530+
isdefined(Main, :main) || return false
531+
M_binding_owner = Base.binding_module(Main, :main)
532+
(isdefined(M_binding_owner, Symbol("#__main_is_entrypoint__#")) && M_binding_owner.var"#__main_is_entrypoint__#") || return false
533+
return true
534+
end
535+
539536
"""
540537
include([mapexpr::Function,] path::AbstractString)
541538
@@ -565,13 +562,111 @@ function _start()
565562
append!(ARGS, Core.ARGS)
566563
# clear any postoutput hooks that were saved in the sysimage
567564
empty!(Base.postoutput_hooks)
565+
local ret = 0
568566
try
569-
exec_options(JLOptions())
567+
repl_was_requested = exec_options(JLOptions())
568+
if should_use_main_entrypoint() && !is_interactive
569+
if Core.Compiler.generating_output()
570+
precompile(Main.main, (typeof(ARGS),))
571+
else
572+
ret = invokelatest(Main.main, ARGS)
573+
end
574+
elseif (repl_was_requested || is_interactive)
575+
# Run the Base `main`, which will either load the REPL stdlib
576+
# or run the fallback REPL
577+
ret = repl_main(ARGS)
578+
end
579+
ret === nothing && (ret = 0)
580+
ret = Cint(ret)
570581
catch
582+
ret = Cint(1)
571583
invokelatest(display_error, scrub_repl_backtrace(current_exceptions()))
572-
exit(1)
573584
end
574585
if is_interactive && get(stdout, :color, false)
575586
print(color_normal)
576587
end
588+
return ret
589+
end
590+
591+
function repl_main(_)
592+
opts = Base.JLOptions()
593+
interactiveinput = isa(stdin, Base.TTY)
594+
b = opts.banner
595+
auto = b == -1
596+
banner = b == 0 || (auto && !interactiveinput) ? :no :
597+
b == 1 || (auto && interactiveinput) ? :yes :
598+
:short # b == 2
599+
600+
quiet = (opts.quiet != 0)
601+
history_file = (opts.historyfile != 0)
602+
color_set = (opts.color != 0) # --color!=auto
603+
return run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
604+
end
605+
606+
"""
607+
@main
608+
609+
This macro is used to mark that the binding `main` in the current module is considered an
610+
entrypoint. The precise semantics of the entrypoint depend on the CLI driver.
611+
612+
In the `julia` driver, if `Main.main` is marked as an entrypoint, it will be automatically called upon
613+
the completion of script execution.
614+
615+
The `@main` macro may be used standalone or as part of the function definition, though in the latter
616+
case, parenthese are required. In particular, the following are equivalent:
617+
618+
```
619+
function (@main)(ARGS)
620+
println("Hello World")
621+
end
622+
```
623+
624+
```
625+
function main(ARGS)
626+
end
627+
@main
628+
```
629+
630+
## Detailed semantics
631+
632+
The entrypoint semantics attach to the owner of the binding owner. In particular, if a marked entrypoint is
633+
imported into `Main`, it will be treated as an entrypoint in `Main`:
634+
635+
```
636+
module MyApp
637+
export main
638+
(@main)(ARGS) = println("Hello World")
639+
end
640+
using .MyApp
641+
# `julia` Will execute MyApp.main at the conclusion of script execution
642+
```
643+
644+
Note that in particular, the semantics do not attach to the method
645+
or the name:
646+
```
647+
module MyApp
648+
(@main)(ARGS) = println("Hello World")
649+
end
650+
const main = MyApp.main
651+
# `julia` Will *NOT* execute MyApp.main unless there is a separate `@main` annotation in `Main`
652+
653+
!!! compat "Julia 1.11"
654+
This macro is new in Julia 1.11. At present, the precise semantics of `@main` are still subject to change.
655+
```
656+
"""
657+
macro main(args...)
658+
if !isempty(args)
659+
error("USAGE: `@main` is expected to be used as `(@main)` without macro arguments.")
660+
end
661+
if isdefined(__module__, :main)
662+
if Base.binding_module(__module__, :main) !== __module__
663+
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module.")
664+
end
665+
end
666+
Core.eval(__module__, quote
667+
# Force the binding to resolve to this module
668+
global main
669+
global var"#__main_is_entrypoint__#"::Bool = true
670+
end)
671+
esc(:main)
577672
end

base/exports.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,9 @@ export
10671067
@goto,
10681068
@view,
10691069
@views,
1070-
@static
1070+
@static,
1071+
1072+
@main
10711073

10721074
# TODO: use normal syntax once JuliaSyntax.jl becomes available at this point in bootstrapping
10731075
eval(Expr(:public,

doc/src/manual/command-line-interface.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,73 @@ $ julia --color=yes -O -- script.jl arg1 arg2..
3939

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

42+
## The `Main.main` entry point
43+
44+
As of Julia, 1.11, Base export a special macro `@main`. This macro simply expands to the symbol `main`,
45+
but at the conclusion of executing a script or expression, `julia` will attempt to execute the function
46+
`Main.main(ARGS)` if such a function has been defined and this behavior was opted into
47+
using the `@main macro`. This feature is intended to aid in the unification
48+
of compiled and interactive workflows. In compiled workflows, loading the code that defines the `main`
49+
function may be spatially and temporally separated from the invocation. However, for interactive workflows,
50+
the behavior is equivalent to explicitly calling `exit(main(ARGS))` at the end of the evaluated script or
51+
expression.
52+
53+
!!! compat "Julia 1.11"
54+
The special entry point `Main.main` was added in Julia 1.11. For compatibility with prior julia versions,
55+
add an explicit `@isdefined(var"@main") ? (@main) : exit(main(ARGS))` at the end of your scripts.
56+
57+
To see this feature in action, consider the following definition, which will execute the print function despite there being no explicit call to `main`:
58+
59+
```
60+
$ julia -e '(@main)(ARGS) = println("Hello World!")'
61+
Hello World!
62+
$
63+
```
64+
65+
Only the `main` binding in the `Main`, module has this special behavior and only if
66+
the macro `@main` was used within the defining module.
67+
68+
For example, using `hello` instead of `main` will result not result in the `hello` function executing:
69+
70+
```
71+
$ julia -e 'hello(ARGS) = println("Hello World!")'
72+
$
73+
```
74+
75+
and neither will a plain definition of `main`:
76+
```
77+
$ julia -e 'main(ARGS) = println("Hello World!")'
78+
$
79+
```
80+
81+
However, the opt-in need not occur at definition time:
82+
$ julia -e 'main(ARGS) = println("Hello World!"); @main'
83+
Hello World!
84+
$
85+
86+
The `main` binding may be imported from a package. A hello package defined as
87+
88+
```
89+
module Hello
90+
91+
export main
92+
(@main)(ARGS) = println("Hello from the package!")
93+
94+
end
95+
```
96+
97+
may be used as:
98+
99+
```
100+
$ julia -e 'using Hello'
101+
Hello from the package!
102+
$ julia -e 'import Hello' # N.B.: Execution depends on the binding not whether the package is loaded
103+
$
104+
```
105+
106+
However, note that the current best practice recommendation is to not mix application and reusable library
107+
code in the same package. Helper applications may be distributed as separate pacakges or as scripts with
108+
separate `main` entry points in a package's `bin` folder.
42109

43110
## Parallel mode
44111

src/jlapi.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,16 +576,20 @@ static NOINLINE int true_main(int argc, char *argv[])
576576

577577
if (start_client) {
578578
jl_task_t *ct = jl_current_task;
579+
int ret = 1;
579580
JL_TRY {
580581
size_t last_age = ct->world_age;
581582
ct->world_age = jl_get_world_counter();
582-
jl_apply(&start_client, 1);
583+
jl_value_t *r = jl_apply(&start_client, 1);
584+
if (jl_typeof(r) != (jl_value_t*)jl_int32_type)
585+
jl_type_error("typeassert", (jl_value_t*)jl_int32_type, r);
586+
ret = jl_unbox_int32(r);
583587
ct->world_age = last_age;
584588
}
585589
JL_CATCH {
586590
jl_no_exc_handler(jl_current_exception(), ct);
587591
}
588-
return 0;
592+
return ret;
589593
}
590594

591595
// run program if specified, otherwise enter REPL

test/cmdlineargs.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,3 +982,15 @@ end
982982
#heap-size-hint, we reserve 250 MB for non GC memory (llvm, etc.)
983983
@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)"
984984
end
985+
986+
## `Main.main` entrypoint
987+
988+
# Basic usage
989+
@test readchomp(`$(Base.julia_cmd()) -e '(@main)(ARGS) = println("hello")'`) == "hello"
990+
991+
# Test ARGS with -e
992+
@test readchomp(`$(Base.julia_cmd()) -e '(@main)(ARGS) = println(ARGS)' a b`) == repr(["a", "b"])
993+
994+
# Test import from module
995+
@test readchomp(`$(Base.julia_cmd()) -e 'module Hello; export main; (@main)(ARGS) = println("hello"); end; using .Hello'`) == "hello"
996+
@test readchomp(`$(Base.julia_cmd()) -e 'module Hello; export main; (@main)(ARGS) = println("hello"); end; import .Hello'`) == ""

0 commit comments

Comments
 (0)