Repository: comonicon/Comonicon.jl (julia)

Issue:
[Feature Request] add dependency specification
If I want to write a script with both GPU and CPU code, then I can avoid using CUDA by using the following grammar.

```julia
@cast :(using CUDA) function f(x)
      # some GPU computation
end

@cast function g(x)
      # some CPU computation
end

@main
```

The `@main` macro calls the required using statement by inspecting `ARG[1]`.

Pull request description:
supersedes #204

Patch:
diff --git a/example/lazyload.jl b/example/lazyload.jl
new file mode 100644
index 00000000..1a50a464
--- /dev/null
+++ b/example/lazyload.jl
@@ -0,0 +1,30 @@
+using Comonicon
+
+@lazyload using Random @cast function random()
+    isdefined(Main, :Random) && println("Random is loaded")
+    isdefined(Main, :LinearAlgebra) && println("LinearAlgebra is loaded")
+end
+
+@lazyload using Random, LinearAlgebra @cast function both()
+    isdefined(Main, :Random) && println("Random is loaded")
+    isdefined(Main, :LinearAlgebra) && println("LinearAlgebra is loaded")
+end
+
+# this will throw an error
+@cast function none()
+    isdefined(Main, :Random) && println("Random is loaded")
+    isdefined(Main, :LinearAlgebra) && println("LinearAlgebra is loaded")
+end
+
+@main
+
+# You will see the following output if you run this script.
+#
+# shell> julia --project example/lazyload.jl random
+# Random is loaded
+
+# shell> julia --project example/lazyload.jl both
+# Random is loaded
+# LinearAlgebra is loaded
+
+# shell> julia --project example/lazyload.jl none
\ No newline at end of file
diff --git a/src/Comonicon.jl b/src/Comonicon.jl
index 539769e8..be2c261a 100644
--- a/src/Comonicon.jl
+++ b/src/Comonicon.jl
@@ -10,7 +10,7 @@ using Pkg
 using Markdown
 using ExproniconLite
 
-export @cast, @main, cmd_error, cmd_exit
+export @cast, @main, cmd_error, cmd_exit, @lazyload
 
 include("compat.jl")
 include("configs.jl")
diff --git a/src/frontend/cast.jl b/src/frontend/cast.jl
index 8ec9a3a4..7204d654 100644
--- a/src/frontend/cast.jl
+++ b/src/frontend/cast.jl
@@ -652,3 +652,44 @@ function cast_options(doc::JLMD, options::Vector{JLOption}, line)
     end
     return cmd_options
 end
+
+"""
+    @lazyload expr @cast <valid castable expr>
+
+Evaluate `expr` if command `f` is called. This is useful
+for reducing the latency of scripts that has subcmds depend
+on plotting, serialization etc.
+
+Please see [`@cast`](@ref) for valid expression specifications.
+
+!!! warning
+
+    This macro only works at top-level command and thus can only be
+    used in `Main` and only works in scripts. Using this macro in any
+    other module or a project module will not work.
+"""
+macro lazyload(load_expr, casted_expr)
+    return esc(lazyload_m(__module__, __source__, load_expr, casted_expr))
+end
+
+function lazyload_m(m::Module, line, load_expr, casted_expr::Expr)
+    m === Main || throw(ArgumentError("`@lazyload` can only be used in `Main`, got $m"))
+    Meta.isexpr(casted_expr, :macrocall) && casted_expr.args[1] === Symbol("@cast") ||
+        throw(ArgumentError("`@lazyload` only accept `@cast`ed objects in `Main`"))
+
+    if is_function(casted_expr.args[3])
+        fname = JLFunction(casted_expr.args[3]).name
+        cmd_name = default_name(fname)
+    elseif Meta.isexpr(casted_expr.args[3], :module)
+        cmd_name = default_name(casted_expr.args[3].args[2])
+    end
+
+    return quote
+        if !isempty(ARGS) && ARGS[1] == $cmd_name
+            $load_expr
+        end
+
+        $line
+        Core.@__doc__ $casted_expr
+    end
+end


Test patch:
diff --git a/test/Project.toml b/test/Project.toml
index 7a3f939e..5c576629 100644
--- a/test/Project.toml
+++ b/test/Project.toml
@@ -1,12 +1,13 @@
 [deps]
-FromFile = "ff7dd447-1dcb-4ce3-b8ac-22a812192de7"
-PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
 ComoniconTestUtils = "53b67e40-53a7-4335-973f-e3e901fe4865"
 ExproniconLite = "55351af7-c7e9-48d6-89ff-24e801d99491"
 Faker = "0efc519c-db33-5916-ab87-703215c3906f"
+FromFile = "ff7dd447-1dcb-4ce3-b8ac-22a812192de7"
+LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
 Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
+PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
 Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+Scratch = "6c6a2e73-6563-6170-7368-637461726353"
 SimpleMock = "a896ed2c-15a5-4479-b61d-a0e88e2a1d25"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
-Scratch = "6c6a2e73-6563-6170-7368-637461726353"
diff --git a/test/examples.jl b/test/examples.jl
new file mode 100644
index 00000000..bc664184
--- /dev/null
+++ b/test/examples.jl
@@ -0,0 +1,20 @@
+using Test
+using Comonicon
+
+example_dir(xs...) = pkgdir(Comonicon, "example", xs...)
+
+function execute(name, args::String="")
+    path = example_dir("$name.jl")
+    cmd = Base.julia_cmd()
+    return run(`$cmd --project=$(Base.active_project()) $(path) $args`)
+end
+
+@testset "lazyload $args" for args in [
+        "",
+        "random",
+        "both",
+        "none",
+    ]
+    p = execute("lazyload", args)
+    @test p.exitcode == 0
+end
diff --git a/test/frontend/markdown.jl b/test/frontend/markdown.jl
index 612a9d2e..fa49a467 100644
--- a/test/frontend/markdown.jl
+++ b/test/frontend/markdown.jl
@@ -235,3 +235,45 @@ end
 end
 
 end # TestMarkdown
+
+module TestLazyLoad
+
+using Test
+using Comonicon
+using ExproniconLite
+
+@testset "lazyload" begin
+    ex = @expr @cast function f()
+    end
+    generated = Comonicon.lazyload_m(Main, nothing, :(using Pkg), ex)
+
+    @test_expr generated == quote
+        if !(isempty(ARGS)) && ARGS[1] == "f"
+            using Pkg
+        end
+
+        Core.@__doc__ @cast function f()
+        end
+    end
+
+    ex = @expr @cast module nodecmd
+        using Comonicon
+        @cast function f()
+        end
+    end
+
+    generated = Comonicon.lazyload_m(Main, nothing, :(using Pkg), ex)
+    @test_expr generated == quote
+        if !(isempty(ARGS)) && ARGS[1] == "nodecmd"
+            using Pkg
+        end
+
+        Core.@__doc__ @cast module nodecmd
+            using Comonicon
+            @cast function f()
+            end
+        end
+    end
+end
+
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index 0a011005..ab8491ff 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -37,6 +37,10 @@ end
     include("scripts.jl")
 end
 
+@testset "example scripts" begin
+    include("examples.jl")
+end
+
 # the script workaround doesn't work on MacOS
 # https://github.com/actions/runner/issues/241
 # @testset "tools" begin