Skip to content

Commit 45708c3

Browse files
committed
support for closures via opaque closures
Should close #28. Kwargs or `where` parameters might not work, but that's an issue in Julia base.
1 parent 65d2451 commit 45708c3

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

src/RuntimeGeneratedFunctions.jl

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export RuntimeGeneratedFunction, @RuntimeGeneratedFunction
77

88
"""
99
@RuntimeGeneratedFunction(function_expression)
10-
@RuntimeGeneratedFunction(context_module, function_expression)
10+
@RuntimeGeneratedFunction(context_module, function_expression, opaque_closures=true)
1111
12-
RuntimeGeneratedFunction(cache_module, context_module, function_expression)
12+
RuntimeGeneratedFunction(cache_module, context_module, function_expression; opaque_closures=true)
1313
1414
Construct a function from `function_expression` which can be called immediately
1515
without world age problems. Somewhat like using `eval(function_expression)` and
@@ -31,6 +31,13 @@ If provided, `context_module` is module in which symbols within
3131
which is currently being precompiled. Normally this would be set to
3232
`@__MODULE__` using one of the macro constructors.
3333
34+
If `opaque_closures` is `true`, all closures in `function_expression` are
35+
converted to
36+
[opaque closures](https://github.com/JuliaLang/julia/pull/37849#issue-496641229).
37+
This allows for the use of closures and generators inside the generated function,
38+
but may not work in all cases due to slightly different semantics. This feature
39+
requires Julia 1.7.
40+
3441
# Examples
3542
```
3643
RuntimeGeneratedFunctions.init(@__MODULE__) # Required at module top-level
@@ -44,9 +51,13 @@ end
4451
"""
4552
struct RuntimeGeneratedFunction{argnames, cache_tag, context_tag, id} <: Function
4653
body::Expr
47-
function RuntimeGeneratedFunction(cache_tag, context_tag, ex)
54+
function RuntimeGeneratedFunction(cache_tag, context_tag, ex; opaque_closures=true)
4855
def = splitdef(ex)
4956
args, body = normalize_args(def[:args]), def[:body]
57+
if opaque_closures && isdefined(Base, :Experimental) &&
58+
isdefined(Base.Experimental, Symbol("@opaque"))
59+
body = closures_to_opaque(body)
60+
end
5061
id = expr_to_id(body)
5162
cached_body = _cache_body(cache_tag, id, body)
5263
new{Tuple(args), cache_tag, context_tag, id}(cached_body)
@@ -73,9 +84,12 @@ macro RuntimeGeneratedFunction(code)
7384
RuntimeGeneratedFunction(@__MODULE__, @__MODULE__, $(esc(code)))
7485
end
7586
end
76-
macro RuntimeGeneratedFunction(context_module, code)
87+
macro RuntimeGeneratedFunction(context_module, code, opaque_closures=true)
7788
quote
78-
RuntimeGeneratedFunction(@__MODULE__, $(esc(context_module)), $(esc(code)))
89+
RuntimeGeneratedFunction(
90+
@__MODULE__, $(esc(context_module)), $(esc(code));
91+
opaque_closures = $(esc(opaque_closures)),
92+
)
7993
end
8094
end
8195

@@ -208,4 +222,51 @@ function expr_to_id(ex)
208222
return Tuple(reinterpret(UInt32, sha1(take!(io))))
209223
end
210224

225+
@nospecialize
226+
227+
closures_to_opaque(x, _=nothing) = x
228+
_tconvert(T, x) = Expr(:(::), Expr(:call, GlobalRef(Base, :convert), T, x), T)
229+
function closures_to_opaque(ex::Expr, return_type=nothing)
230+
head, args = ex.head, ex.args
231+
fdef = splitdef(ex; throw=false)
232+
if fdef !== nothing
233+
body = get(fdef, :body, nothing)
234+
if haskey(fdef, :rtype)
235+
body = _tconvert(fdef[:rtype], closures_to_opaque(body, fdef[:rtype]))
236+
delete!(fdef, :rtype)
237+
else
238+
body = closures_to_opaque(body)
239+
end
240+
fdef[:head] = :(->)
241+
fdef[:body] = body
242+
name = get(fdef, :name, nothing)
243+
name !== nothing && delete!(fdef, :name)
244+
_ex = Expr(:opaque_closure, combinedef(fdef))
245+
# TODO: emit named opaque closure for better stacktraces
246+
# (ref https://github.com/JuliaLang/julia/pull/40242)
247+
if name !== nothing
248+
name isa Symbol ||
249+
error("Unsupported function definition `$ex` in RuntimeGeneratedFunction.")
250+
_ex = Expr(:(=), name, _ex)
251+
end
252+
return _ex
253+
elseif head === :generator
254+
f_args = Expr(:tuple, Any[x.args[1] for x in args[2:end]]...)
255+
iters = Any[x.args[2] for x in args[2:end]]
256+
return Expr(
257+
:call,
258+
GlobalRef(Base, :Generator),
259+
closures_to_opaque(Expr(:(->), f_args, args[1])),
260+
iters...,
261+
)
262+
elseif head === :opaque_closure
263+
return closures_to_opaque(args[1])
264+
elseif head === :return && return_type !== nothing
265+
return Expr(:return, _tconvert(return_type, closures_to_opaque(args[1], return_type)))
266+
end
267+
return Expr(head, Any[closures_to_opaque(x, return_type) for x in args]...)
268+
end
269+
270+
@specialize
271+
211272
end

test/runtests.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,28 @@ f_outside = @RuntimeGeneratedFunction(GlobalsTest, :(x->x + y_in_GlobalsTest))
133133
# RuntimeGeneratedFunctions.init(@__MODULE__) # <-- missing
134134
f = @RuntimeGeneratedFunction(:(x->x+y))
135135
end)
136+
137+
# closures
138+
if VERSION >= v"1.7.0-DEV.351"
139+
ex = :(x -> (y -> x + y))
140+
@test @RuntimeGeneratedFunction(ex)(2)(3) === 5
141+
142+
ex = :(x -> (f(y::Int)::Float64 = x + y; f))
143+
@test @RuntimeGeneratedFunction(ex)(2)(3) === 5.0
144+
145+
ex = :(x -> function (y::Int)
146+
return x + y
147+
end)
148+
@test @RuntimeGeneratedFunction(ex)(2)(3) === 5
149+
150+
ex = :(x -> function f(y::Int)::UInt8
151+
return x + y
152+
end)
153+
@test @RuntimeGeneratedFunction(ex)(2)(3) === 0x05
154+
155+
ex = :(x -> sum(i^2 for i in 1:x))
156+
@test @RuntimeGeneratedFunction(ex)(3) === 14
157+
158+
ex = :(x -> [2i for i in 1:x])
159+
@test @RuntimeGeneratedFunction(ex)(3) == [2, 4, 6]
160+
end

0 commit comments

Comments
 (0)