Skip to content

Commit b2d43d3

Browse files
Merge pull request #6 from c42f/cjf/support-precompilation
Support for precompilation
2 parents 21b31ad + 1878cfb commit b2d43d3

File tree

4 files changed

+113
-16
lines changed

4 files changed

+113
-16
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
`RuntimeGeneratedFunctions` are functions generated at runtime without world-age
66
issues and with the full performance of a standard Julia anonymous function. This
7-
builds functions in a way that avoids `eval`, but cannot store the precompiled
8-
functions between Julia sessions.
7+
builds functions in a way that avoids `eval`.
98

109
Note that `RuntimeGeneratedFunction` does not handle closures. Please use the
1110
[GeneralizedGenerated.jl](https://github.com/JuliaStaging/GeneralizedGenerated.jl)
@@ -23,7 +22,7 @@ function no_worldage()
2322
@inbounds _du[2] = _u[2]
2423
nothing
2524
end)
26-
f1 = RuntimeGeneratedFunction(ex)
25+
f1 = @RuntimeGeneratedFunction(ex)
2726
du = rand(2)
2827
u = rand(2)
2928
p = nothing

src/RuntimeGeneratedFunctions.jl

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,106 @@ module RuntimeGeneratedFunctions
22

33
using ExprTools, Serialization, SHA
44

5-
const function_cache = Dict{Tuple,Expr}()
6-
struct RuntimeGeneratedFunction{uuid,argnames}
7-
function RuntimeGeneratedFunction(ex)
5+
export @RuntimeGeneratedFunction
6+
7+
8+
"""
9+
RuntimeGeneratedFunction
10+
11+
This type should be constructed via the macro @RuntimeGeneratedFunction.
12+
"""
13+
struct RuntimeGeneratedFunction{moduletag,id,argnames}
14+
function RuntimeGeneratedFunction(moduletag, ex)
815
def = splitdef(ex)
916
args, body = normalize_args(def[:args]), def[:body]
10-
uuid = expr2bytes(body)
11-
function_cache[uuid] = body
12-
new{uuid,Tuple(args)}()
17+
id = expr2bytes(body)
18+
_cache_body(moduletag, id, body)
19+
new{moduletag,id,Tuple(args)}()
20+
end
21+
end
22+
23+
"""
24+
@RuntimeGeneratedFunction(function_expression)
25+
26+
Construct a function from `function_expression` which can be called immediately
27+
without world age problems. Somewhat like using `eval(function_expression)` and
28+
then calling the resulting function. The differences are:
29+
30+
* The result can be called immediately (immune to world age errors)
31+
* The result is not a named generic function, and doesn't participate in
32+
generic function dispatch; it's more like a callable method.
33+
34+
# Examples
35+
```
36+
function foo()
37+
expression = :((x,y)->x+y+1) # May be generated dynamically
38+
f = @RuntimeGeneratedFunction(expression)
39+
f(1,2) # May be called immediately
40+
end
41+
```
42+
"""
43+
macro RuntimeGeneratedFunction(ex)
44+
_ensure_cache_exists!(__module__)
45+
quote
46+
RuntimeGeneratedFunction(
47+
$(esc(_tagname)),
48+
$(esc(ex))
49+
)
1350
end
1451
end
52+
53+
function Base.show(io::IO, f::RuntimeGeneratedFunction{moduletag, id, argnames}) where {moduletag,id,argnames}
54+
body = _lookup_body(moduletag, id)
55+
mod = parentmodule(moduletag)
56+
func_expr = Expr(:->, Expr(:tuple, argnames...), body)
57+
print(io, "RuntimeGeneratedFunction(#=in $mod=#, ", repr(func_expr), ")")
58+
end
59+
1560
(f::RuntimeGeneratedFunction)(args::Vararg{Any,N}) where N = generated_callfunc(f, args...)
1661

17-
@inline @generated function generated_callfunc(f::RuntimeGeneratedFunction{uuid,argnames},__args...) where {uuid,argnames}
62+
@inline @generated function generated_callfunc(f::RuntimeGeneratedFunction{moduletag, id, argnames}, __args...) where {moduletag,id,argnames}
1863
setup = (:($(argnames[i]) = @inbounds __args[$i]) for i in 1:length(argnames))
1964
quote
2065
$(setup...)
21-
$(function_cache[uuid])
66+
$(_lookup_body(moduletag, id))
2267
end
2368
end
2469

25-
export RuntimeGeneratedFunction
70+
### Function body caching and lookup
71+
#
72+
# Caching the body of a RuntimeGeneratedFunction is a little complicated
73+
# because we want the `id=>body` mapping to survive precompilation. This means
74+
# we need to store the cache of mappings which are created by a module in that
75+
# module itself.
76+
#
77+
# For that, we need a way to lookup the correct module from an instance of
78+
# RuntimeGeneratedFunction. Modules can't be type parameters, but we can use
79+
# any type which belongs to the module as a proxy "tag" for the module.
80+
#
81+
# (We could even abuse `typeof(__module__.eval)` for the tag, though this is a
82+
# little non-robust to weird special cases like Main.eval being
83+
# Base.MainInclude.eval.)
84+
85+
_cachename = Symbol("#_RuntimeGeneratedFunctions_cache")
86+
_tagname = Symbol("#_RuntimeGeneratedFunctions_ModTag")
87+
88+
function _cache_body(moduletag, id, body)
89+
getfield(parentmodule(moduletag), _cachename)[id] = body
90+
end
91+
92+
function _lookup_body(moduletag, id)
93+
getfield(parentmodule(moduletag), _cachename)[id]
94+
end
95+
96+
function _ensure_cache_exists!(mod)
97+
if !isdefined(mod, _cachename)
98+
mod.eval(quote
99+
const $_cachename = Dict{Tuple,Expr}()
100+
struct $_tagname
101+
end
102+
end)
103+
end
104+
end
26105

27106
###
28107
### Utilities

test/precomp/RGFPrecompTest.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module RGFPrecompTest
2+
using RuntimeGeneratedFunctions
3+
4+
f = @RuntimeGeneratedFunction(:((x,y)->x+y))
5+
end

test/runtests.jl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ ex3 = :(function (_du::T,_u::Vector{E},_p::P,_t::Any) where {T<:Vector,E,P}
2525
nothing
2626
end)
2727

28-
f1 = RuntimeGeneratedFunction(ex1)
29-
f2 = RuntimeGeneratedFunction(ex2)
30-
f3 = RuntimeGeneratedFunction(ex3)
28+
f1 = @RuntimeGeneratedFunction(ex1)
29+
f2 = @RuntimeGeneratedFunction(ex2)
30+
f3 = @RuntimeGeneratedFunction(ex3)
3131

3232
du = rand(2)
3333
u = rand(2)
@@ -58,11 +58,25 @@ function no_worldage()
5858
@inbounds _du[2] = _u[2]
5959
nothing
6060
end)
61-
f1 = RuntimeGeneratedFunction(ex)
61+
f1 = @RuntimeGeneratedFunction(ex)
6262
du = rand(2)
6363
u = rand(2)
6464
p = nothing
6565
t = nothing
6666
f1(du,u,p,t)
6767
end
6868
@test no_worldage() === nothing
69+
70+
# Test show()
71+
@test sprint(show, @RuntimeGeneratedFunction(Base.remove_linenums!(:((x,y)->x+y+1)))) ==
72+
"""
73+
RuntimeGeneratedFunction(#=in $(@__MODULE__)=#, :((x, y)->begin
74+
x + y + 1
75+
end))"""
76+
77+
# Test with precompilation
78+
push!(LOAD_PATH, joinpath(@__DIR__, "precomp"))
79+
using RGFPrecompTest
80+
81+
@test RGFPrecompTest.f(1,2) == 3
82+

0 commit comments

Comments
 (0)