Skip to content

Commit 6550973

Browse files
Migrate to strict SciMLTesting 2.4 (#136)
* Migrate RuntimeGeneratedFunctions to strict SciMLTesting 2.4 Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * Format serialization error with Runic Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
1 parent 18e4b5d commit 6550973

9 files changed

Lines changed: 158 additions & 99 deletions

File tree

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "RuntimeGeneratedFunctions"
22
uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
33
authors = ["Chris Rackauckas <accounts@chrisrackauckas.com> and contributors"]
4-
version = "0.5.22"
4+
version = "0.6.0"
55

66
[deps]
77
ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
@@ -13,7 +13,7 @@ BenchmarkTools = "1"
1313
ExprTools = "0.1"
1414
SHA = "0.7, 1"
1515
SafeTestsets = "0.1, 1"
16-
SciMLTesting = "2.1"
16+
SciMLTesting = "2.4"
1717
Serialization = "1.10"
1818
Test = "1"
1919
julia = "1.10"

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
44

55
[compat]
66
Documenter = "1"
7-
RuntimeGeneratedFunctions = "0.5"
7+
RuntimeGeneratedFunctions = "0.6"

docs/make.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ makedocs(
88
sitename = "RuntimeGeneratedFunctions.jl",
99
authors = "Chris Rackauckas",
1010
modules = [RuntimeGeneratedFunctions],
11-
clean = true, doctest = false, linkcheck = true,
11+
clean = true,
12+
doctest = true,
13+
linkcheck = true,
14+
checkdocs = :exports,
1215
format = Documenter.HTML(
1316
assets = ["assets/favicon.ico"],
1417
canonical = "https://docs.sciml.ai/RuntimeGeneratedFunctions/stable/"

docs/src/api.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
# API
1+
# Public API
22

3-
```@autodocs
4-
Modules = [RuntimeGeneratedFunctions]
3+
```@docs
4+
RuntimeGeneratedFunction
5+
@RuntimeGeneratedFunction
6+
drop_expr
7+
```
8+
9+
# Setup and Inspection API
10+
11+
```@docs
12+
RuntimeGeneratedFunctions.init
13+
RuntimeGeneratedFunctions.get_expression
514
```

src/RuntimeGeneratedFunctions.jl

Lines changed: 122 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module RuntimeGeneratedFunctions
22

33
using ExprTools: ExprTools, combinedef, splitdef
44
using SHA: SHA, SHA1_CTX, update!
5-
using Serialization: Serialization, AbstractSerializer, deserialize, serialize
5+
using Serialization: Serialization, AbstractSerializer, deserialize
66

77
export RuntimeGeneratedFunction, @RuntimeGeneratedFunction, drop_expr
88

@@ -15,52 +15,53 @@ export RuntimeGeneratedFunction, @RuntimeGeneratedFunction, drop_expr
1515
eval(Expr(:public, :init, :get_expression))
1616
end
1717

18-
const _rgf_docs = """
19-
@RuntimeGeneratedFunction(function_expression)
20-
@RuntimeGeneratedFunction(context_module, function_expression, opaque_closures=true)
18+
"""
19+
RuntimeGeneratedFunction(cache_module, context_module, function_expression;
20+
opaque_closures = true)
21+
22+
A callable function compiled from `function_expression` without a world-age
23+
barrier. A `RuntimeGeneratedFunction` is a `Function`, so generic code that
24+
accepts and invokes a `Function` can use it. It is not a named generic function:
25+
it represents one callable method and does not participate in method dispatch.
26+
27+
# Arguments
2128
22-
RuntimeGeneratedFunction(cache_module, context_module, function_expression; opaque_closures=true)
29+
- `cache_module::Module`: module that owns the cached expression. It must have
30+
been initialized with [`init`](@ref). During precompilation, pass the module
31+
currently being precompiled so the cache survives precompilation.
32+
- `context_module::Module`: module in which global names in
33+
`function_expression` are resolved. It must have been initialized with
34+
[`init`](@ref).
35+
- `function_expression::Expr`: an anonymous-function or function-definition
36+
expression whose body becomes the callable function.
2337
24-
Construct a function from `function_expression` which can be called immediately
25-
without world age problems. Somewhat like using `eval(function_expression)` and
26-
then calling the resulting function. The differences are:
38+
# Keywords
2739
28-
* The result can be called immediately (immune to world age errors)
29-
* The result is not a named generic function, and doesn't participate in
30-
generic function dispatch; it's more like a callable method.
40+
- `opaque_closures = true`: convert closures and generators in
41+
`function_expression` to Julia opaque closures. This permits closures in the
42+
generated body, with Julia's opaque-closure semantics.
3143
32-
You need to use `RuntimeGeneratedFunctions.init(your_module)` a single time at
33-
the top level of `your_module` before any other uses of the macro.
44+
# Fields
3445
35-
If provided, `context_module` is the module in which symbols within
36-
`function_expression` will be looked up. By default, this is the module in which
37-
`@RuntimeGeneratedFunction` is expanded.
46+
- `body`: cached body expression while it is retained, or `nothing` after
47+
[`drop_expr`](@ref). The function remains callable after dropping its local
48+
expression reference because the cache owns the body.
3849
39-
`cache_module` is the module where the expression `code` will be cached. If
40-
`RuntimeGeneratedFunction` is used during precompilation, this must be a module
41-
which is currently being precompiled. Normally this would be set to
42-
`@__MODULE__` using one of the macro constructors.
50+
# Serialization
51+
52+
`Serialization.serialize` preserves a runtime-generated function while its
53+
`body` is retained. Serialize a function before calling [`drop_expr`](@ref): a
54+
dropped function intentionally does not retain the expression needed to restore
55+
its cache in another process.
4356
44-
If `opaque_closures` is `true`, all closures in `function_expression` are
45-
converted to
46-
[opaque closures](https://github.com/JuliaLang/julia/pull/37849#issue-496641229).
47-
This allows for the use of closures and generators inside the generated function,
48-
but may not work in all cases due to slightly different semantics.
4957
# Examples
50-
```
51-
RuntimeGeneratedFunctions.init(@__MODULE__) # Required at module top-level
5258
53-
function foo()
54-
expression = :((x,y)->x+y+1) # May be generated dynamically
55-
f = @RuntimeGeneratedFunction(expression)
56-
f(1,2) # May be called immediately
57-
end
59+
```julia
60+
RuntimeGeneratedFunctions.init(@__MODULE__)
61+
square = RuntimeGeneratedFunction(@__MODULE__, @__MODULE__, :(x -> x^2))
62+
square(3)
5863
```
5964
"""
60-
61-
"""
62-
$_rgf_docs
63-
"""
6465
struct RuntimeGeneratedFunction{argnames, cache_tag, context_tag, id, B} <: Function
6566
body::B
6667
function RuntimeGeneratedFunction(cache_tag, context_tag, ex; opaque_closures = true)
@@ -94,13 +95,26 @@ end
9495
"""
9596
drop_expr(rgf::RuntimeGeneratedFunction)
9697
97-
Return a new `RuntimeGeneratedFunction` that does not hold a reference to the
98-
function body expression. This allows the expression AST to be garbage collected
99-
while keeping the function callable.
98+
Return a copy of `rgf` that does not retain its function body expression.
99+
This allows the expression AST to be garbage collected while keeping the
100+
function callable.
101+
102+
# Arguments
103+
104+
- `rgf::RuntimeGeneratedFunction`: generated function whose retained expression
105+
should be released.
106+
107+
# Returns
108+
109+
- A `RuntimeGeneratedFunction` with `body === nothing`. It has the same callable
110+
behavior as `rgf` while its cache entry remains available.
100111
101112
The expression can still be retrieved later using [`get_expression`](@ref) as long
102113
as at least one `RuntimeGeneratedFunction` with the same body exists.
103114
115+
Serialize a generated function before calling `drop_expr`. A dropped function has
116+
discarded the expression needed to restore its cache in another process.
117+
104118
# Examples
105119
```julia
106120
ex = :((x) -> x^2)
@@ -159,7 +173,34 @@ function RuntimeGeneratedFunction(
159173
end
160174

161175
"""
162-
$_rgf_docs
176+
@RuntimeGeneratedFunction(function_expression)
177+
@RuntimeGeneratedFunction(context_module, function_expression,
178+
opaque_closures = true)
179+
180+
Construct a [`RuntimeGeneratedFunction`](@ref) from a function expression using
181+
the calling module as its cache module. Call [`init`](@ref) at the top level of
182+
that module before expanding this macro.
183+
184+
# Arguments
185+
186+
- `function_expression::Expr`: an anonymous-function or function-definition
187+
expression to compile.
188+
- `context_module::Module`: optional module in which global names in
189+
`function_expression` are resolved. By default, names resolve in the calling
190+
module.
191+
192+
# Keywords
193+
194+
- `opaque_closures = true`: convert closures and generators in the expression to
195+
Julia opaque closures.
196+
197+
# Examples
198+
199+
```julia
200+
RuntimeGeneratedFunctions.init(@__MODULE__)
201+
increment = @RuntimeGeneratedFunction(:(x -> x + 1))
202+
increment(2)
203+
```
163204
"""
164205
macro RuntimeGeneratedFunction(code)
165206
return quote
@@ -276,8 +317,29 @@ end
276317
"""
277318
RuntimeGeneratedFunctions.init(mod)
278319
279-
Use this at top level to set up your module `mod` before using
280-
`@RuntimeGeneratedFunction`.
320+
Initialize `mod` for runtime-generated functions.
321+
322+
# Arguments
323+
324+
- `mod::Module`: module that will cache generated expression bodies and own the
325+
generated call method.
326+
327+
# Rules
328+
329+
Call `init` once at module top level before using
330+
[`@RuntimeGeneratedFunction`](@ref) in `mod`, or before passing `mod` as either
331+
the cache or context module to [`RuntimeGeneratedFunction`](@ref). Repeated calls
332+
are idempotent. Do not call it from a local scope: it defines a generated method
333+
in `mod` so that global names resolve in that module.
334+
335+
# Examples
336+
337+
```julia
338+
module GeneratedFunctionsExample
339+
using RuntimeGeneratedFunctions
340+
RuntimeGeneratedFunctions.init(@__MODULE__)
341+
end
342+
```
281343
"""
282344
function init(mod)
283345
return lock(_cache_lock) do
@@ -412,6 +474,16 @@ end
412474
413475
Retrieve the function expression from a `RuntimeGeneratedFunction`.
414476
477+
# Arguments
478+
479+
- `rgf::RuntimeGeneratedFunction`: generated function whose source expression is
480+
requested.
481+
482+
# Returns
483+
484+
- An anonymous-function `Expr` containing the normalized argument names and the
485+
cached body.
486+
415487
This works even if [`drop_expr`](@ref) has been called on the function, as long as
416488
the expression is still in the cache (i.e., at least one `RuntimeGeneratedFunction`
417489
with the same body exists).
@@ -439,33 +511,6 @@ function get_expression(
439511
return func_expr = Expr(:->, Expr(:tuple, argnames...), _lookup_body(cache_tag, id))
440512
end
441513

442-
# We write an explicit serialize() and deserialize() here to manage caching of
443-
# the body on a remote node when using Serialization.jl (in Distributed.jl
444-
# and elsewhere)
445-
function Serialization.serialize(
446-
s::AbstractSerializer,
447-
rgf::RuntimeGeneratedFunction{
448-
argnames, cache_tag,
449-
context_tag, id, B,
450-
}
451-
) where {
452-
argnames,
453-
cache_tag,
454-
context_tag,
455-
id,
456-
B,
457-
}
458-
body = _lookup_body(cache_tag, id)
459-
Serialization.serialize_type(
460-
s,
461-
RuntimeGeneratedFunction{
462-
argnames, cache_tag, context_tag,
463-
id, B,
464-
}
465-
)
466-
return serialize(s, body)
467-
end
468-
469514
function Serialization.deserialize(
470515
s::AbstractSerializer,
471516
::Type{
@@ -482,13 +527,18 @@ function Serialization.deserialize(
482527
B,
483528
}
484529
body = deserialize(s)
530+
B === Nothing && throw(
531+
ArgumentError(
532+
"cannot deserialize a dropped RuntimeGeneratedFunction; serialize it before calling drop_expr"
533+
)
534+
)
485535
cached_body = _cache_body(cache_tag, id, body)
486536
f = RuntimeGeneratedFunction{argnames, cache_tag, context_tag, id}(cached_body)
487-
return B === Nothing ? drop_expr(f) : f
537+
return f
488538
end
489539

490-
# achieve deepcopy(f)===f behavior similar to "normal" julia functions
491-
Base.deepcopy_internal(f::RuntimeGeneratedFunction, stackdict::IdDict) = f
540+
# Match Julia functions: runtime-generated functions are immutable callable values.
541+
Base.deepcopy(f::RuntimeGeneratedFunction) = f
492542

493543
@specialize
494544

test/core_tests.jl

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ f3 = @RuntimeGeneratedFunction(ex3)
4343

4444
@test f1 isa Function
4545

46+
function evaluate_through_function_interface(f::Function, value)
47+
return f(value)
48+
end
49+
50+
@test evaluate_through_function_interface(@RuntimeGeneratedFunction(:(x -> x + 1)), 41) == 42
51+
4652
du = rand(2)
4753
u = rand(2)
4854
p = nothing
@@ -204,12 +210,16 @@ ex = :(x -> [2i for i in 1:x])
204210

205211
proj = dirname(Base.active_project())
206212
serialize_script = joinpath(@__DIR__, "shared", "serialize_rgf.jl")
207-
buf = IOBuffer(read(`$(Base.julia_cmd()) --startup-file=no --project=$proj $serialize_script`))
208-
deserialized_f, deserialized_g = deserialize(buf)
213+
julia = joinpath(Sys.BINDIR, "julia")
214+
buf = IOBuffer(read(`$julia --startup-file=no --project=$proj $serialize_script`))
215+
deserialized_f = deserialize(buf)
209216
@test deserialized_f(11) == "Hi from a separate process. x=11"
210217
@test deserialized_f.body isa Expr
211-
@test deserialized_g(12) == "Serialization with dropped body. y=12"
212-
@test deserialized_g.body isa Nothing
218+
219+
serialization_buffer = IOBuffer()
220+
serialize(serialization_buffer, drop_expr(@RuntimeGeneratedFunction(:(x -> x + 1))))
221+
seekstart(serialization_buffer)
222+
@test_throws ArgumentError deserialize(serialization_buffer)
213223

214224
# deepcopy
215225
ff = @RuntimeGeneratedFunction(:(x -> [x, x + 1]))

test/qa/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
88
[compat]
99
Aqua = "0.8"
1010
SafeTestsets = "0.1, 1"
11-
SciMLTesting = "2.1"
11+
SciMLTesting = "2.4"
1212
Test = "1"
1313
julia = "1.10"

test/qa/qa.jl

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
11
using RuntimeGeneratedFunctions, SciMLTesting, Test
22

3-
# `Base.deepcopy_internal` and `Serialization.serialize_type` are the
4-
# documented-by-convention extension points used to customize `deepcopy` and
5-
# `Serialization.serialize`, respectively. Neither owner declares them public, but
6-
# overriding/calling them is the only supported way to get the required behavior,
7-
# so they are ignored rather than rewritten.
8-
run_qa(
9-
RuntimeGeneratedFunctions;
10-
explicit_imports = true,
11-
api_docs_kwargs = (; rendered = true),
12-
ei_kwargs = (;
13-
all_qualified_accesses_are_public = (; ignore = (:deepcopy_internal, :serialize_type)),
14-
),
15-
)
3+
run_qa(RuntimeGeneratedFunctions)

test/shared/serialize_rgf.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ using Serialization
66
RuntimeGeneratedFunctions.init(@__MODULE__)
77

88
f = @RuntimeGeneratedFunction(:(x -> "Hi from a separate process. x=$x"))
9-
g = drop_expr(@RuntimeGeneratedFunction(:(y -> "Serialization with dropped body. y=$y")))
109

11-
serialize(stdout, (f, g))
10+
serialize(stdout, f)

0 commit comments

Comments
 (0)