From 7692b6959f35e5e088f717b541112989e2e53416 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 5 Aug 2015 15:09:37 -0400 Subject: [PATCH] add "#pragma compilable [=true]" for opt-in to automatic compilation when a module is required (closes #12462) --- NEWS.md | 3 ++ base/loading.jl | 73 ++++++++++++++++++++++++++++++++++++------ doc/manual/modules.rst | 10 +++++- test/compile.jl | 10 ++++-- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/NEWS.md b/NEWS.md index 458bd5d60d89fa..17a3de40e1d59b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -32,6 +32,8 @@ New language features but instead of loading it into the current session saves the result of compiling it in `~/.julia/lib/v0.4` ([#8745]). + * Put `#pragma compilable` at the top of your module file to automatically compile it when it is imported ([#12475]). + * See manual section on `Module initialization and precompilation` (under `Modules`) for details and errata. * New option `--output-incremental={yes|no}` added to invoke the equivalent of ``Base.compile`` from the command line. @@ -1557,3 +1559,4 @@ Too numerous to mention. [#12137]: https://github.com/JuliaLang/julia/issues/12137 [#12162]: https://github.com/JuliaLang/julia/issues/12162 [#12393]: https://github.com/JuliaLang/julia/issues/12393 +[#12475]: https://github.com/JuliaLang/julia/issues/12475 diff --git a/base/loading.jl b/base/loading.jl index b8005e7bcc252c..4b1bfc728336e8 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -53,6 +53,45 @@ function find_all_in_cache_path(mod::Symbol) paths end +const r_compilable = r"^#\s*pragma\s+compilable(\s*=\s*true)?\s*$" +const r_ncompilable = r"^#\s*pragma\s+compilable\s*=\s*false\s*$" + +# return true if "#pragma compilable [=true]" appears in the file before +# any Julia code, false for "#pragma compilable=false", default otherwise +function compilable(path::AbstractString, default::Bool=false) + return open(path, "r") do f + for line in eachline(f) + s = lstrip(line) + if !isempty(s) + if s[1] == '#' + ismatch(r_compilable, s) && return true + ismatch(r_ncompilable, s) && return false + else + return default + end + end + end + return default + end +end + +# compile path on node 1 if path is #pragma compilable, +# returning the cachefile path, or nothing otherwise +function autocompile_on_node1(mod::Symbol, path::AbstractString) + if myid() == 1 + if compilable(path) + if isinteractive() + info("Compiling module $mod from $path...") + end + return compile(mod) + else + return nothing + end + else + return remotecall_fetch(1, autocompile_on_node1, mod, path) + end +end + function _include_from_serialized(content::Vector{UInt8}) return ccall(:jl_restore_incremental_from_buf, Any, (Ptr{Uint8},Int), content, sizeof(content)) end @@ -84,6 +123,14 @@ function _require_from_serialized(node::Int, path_to_try::ByteString, toplevel_l restored = _include_from_serialized(content) end # otherwise, continue search + + if restored !== nothing + for M in restored + if isdefined(M, :__META__) + push!(Base.Docs.modules, M) + end + end + end return restored end @@ -119,15 +166,9 @@ function require(mod::Symbol) last = toplevel_load::Bool try toplevel_load = false - restored = _require_from_serialized(1, mod, last) - if restored !== nothing - for M in restored - if isdefined(M, :__META__) - push!(Base.Docs.modules, M) - end - end - return true - end + if nothing !== _require_from_serialized(1, mod, last) + return + end if JLOptions().incremental != 0 # spawn off a new incremental compile task from node 1 for recursive `require` calls cachefile = compile(mod) @@ -140,6 +181,19 @@ function require(mod::Symbol) name = string(mod) path = find_in_node_path(name, source_dir(), 1) path === nothing && throw(ArgumentError("$name not found in path")) + + if last || nprocs() == 1 + cachefile = autocompile_on_node1(mod, path) + if cachefile !== nothing + if nothing === _require_from_serialized(1, cachefile, last) + warn("require failed to create a precompiled cache file") + else + return + end + end + end + + # could not compile, just include(path) if last && myid() == 1 && nprocs() > 1 # broadcast top-level import/using from node 1 (only) content = open(readall, path) @@ -262,6 +316,7 @@ function compile(name::ByteString) myid() == 1 || error("can only compile from node 1") path = find_in_path(name) path === nothing && throw(ArgumentError("$name not found in path")) + !compilable(path, true) && throw(ArgumentError("$name has #pragma compilable=false")) cachepath = LOAD_CACHE_PATH[1] if !isdir(cachepath) mkpath(cachepath) diff --git a/doc/manual/modules.rst b/doc/manual/modules.rst index 2b81c0158fe2dc..c938e1c0213416 100644 --- a/doc/manual/modules.rst +++ b/doc/manual/modules.rst @@ -264,7 +264,15 @@ To create a custom system image that can be used to start julia with the -J opti recompile Julia after modifying the file ``base/userimg.jl`` to require the desired modules. To create an incremental precompiled module file, -call ``Base.compile(modulename::Symbol)``. +you can call ``Base.compile(modulename::Symbol)``. Alternatively, if you +put ``#pragma compilable`` at the top of your module file (before any +non-comment code), then the module will be automatically compiled the +first time it is imported. Compiling a module also recursively compiles +any modules that are imported therein. If you know that it is *not* +safe to compile your module, you should put ``#pragma compilable=false`` +at the top of its file, which cause ``Base.compile`` to throw an error +(and thereby prevent the module from being imported any other compiled +module). The resulting cache files will be stored in ``Base.LOAD_CACHE_PATH[1]``. In order to make your module work with precompilation, diff --git a/test/compile.jl b/test/compile.jl index 8fda7e0cae8c9e..3c0ab3b9720464 100644 --- a/test/compile.jl +++ b/test/compile.jl @@ -11,6 +11,8 @@ try open(file, "w") do f print(f, """ + # Auto-compile this module: + # pragma compilable module $Foo_module @doc "foo function" foo(x) = x + 1 module Bar @@ -20,8 +22,12 @@ try """) end - Base.compile(Foo_module) - eval(Main, :(import $Foo_module)) + # make sure the "#pragma compilable" causes Foo to be compiled + cachefile = Base.autocompile_on_node1(Foo_module, file) + @test cachefile !== nothing + + # make sure the compiled module is successfully loaded: + @test nothing !== Base._require_from_serialized(1, cachefile, true) finally splice!(Base.LOAD_CACHE_PATH, 1) splice!(LOAD_PATH, 1)