Skip to content

Commit f7b30d2

Browse files
committed
Front end macro and lowering of cfunction
Quoting of the callable seems broken with respect to macro hygiene, but for now we follow the Julia reference implementation and defer fixing this to later. See also issue c42f#9.
1 parent 8ef53a0 commit f7b30d2

File tree

6 files changed

+131
-16
lines changed

6 files changed

+131
-16
lines changed

src/eval.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
290290
k == K"gc_preserve_begin" ? :gc_preserve_begin :
291291
k == K"gc_preserve_end" ? :gc_preserve_end :
292292
k == K"foreigncall" ? :foreigncall :
293+
k == K"cfunction" ? :cfunction :
293294
k == K"opaque_closure_method" ? :opaque_closure_method :
294295
nothing
295296
if isnothing(head)

src/kinds.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ function _register_kinds()
2929
"loopinfo"
3030
# Call into foreign code. Emitted by `@ccall`
3131
"foreigncall"
32+
# Special form for constructing a function callable from C
33+
"cfunction"
3234
# Special form emitted by `Base.Experimental.@opaque`
3335
"opaque_closure"
3436
# Test whether a variable is defined

src/linear_ir.jl

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,17 @@ function is_valid_ir_rvalue(ctx, lhs, rhs)
143143
return is_ssa(ctx, lhs) ||
144144
is_valid_ir_argument(ctx, rhs) ||
145145
(kind(lhs) == K"BindingId" &&
146-
# FIXME: add: invoke cfunction gc_preserve_begin copyast
147-
kind(rhs) in KSet"new splatnew isdefined call foreigncall gc_preserve_begin foreigncall new_opaque_closure")
146+
# FIXME: add: invoke ?
147+
kind(rhs) in KSet"new splatnew cfunction isdefined call foreigncall gc_preserve_begin foreigncall new_opaque_closure")
148148
end
149149

150-
function contains_nonglobal_binding(ctx, ex)
151-
contains_unquoted(ex) do e
150+
function check_no_local_bindings(ctx, ex, msg)
151+
contains_nonglobal_binding = contains_unquoted(ex) do e
152152
kind(e) == K"BindingId" && lookup_binding(ctx, e).kind !== :global
153153
end
154+
if contains_nonglobal_binding
155+
throw(LoweringError(ex, msg))
156+
end
154157
end
155158

156159
# evaluate the arguments of a call, creating temporary locations as needed
@@ -582,18 +585,15 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
582585
@chk !needs_value (ex,"TOMBSTONE encountered in value position")
583586
nothing
584587
elseif k == K"call" || k == K"new" || k == K"splatnew" || k == K"foreigncall" ||
585-
k == K"new_opaque_closure"
586-
# TODO k ∈ cfunction cglobal
588+
k == K"new_opaque_closure" || k == K"cfunction"
587589
if k == K"foreigncall"
588590
args = SyntaxList(ctx)
589591
# todo: is is_leaf correct here? flisp uses `atom?`
590592
func = ex[1]
591593
if kind(func) == K"call" && kind(func[1]) == K"core" && func[1].name_val == "tuple"
592594
# Tuples like core.tuple(:funcname, mylib_name) are allowed,
593595
# but may only reference globals.
594-
if contains_nonglobal_binding(ctx, func)
595-
throw(LoweringError(func, "ccall function name and library expression cannot reference local variables"))
596-
end
596+
check_no_local_bindings(ctx, func, "ccall function name and library expression cannot reference local variables")
597597
append!(args, compile_args(ctx, ex[1:1]))
598598
elseif is_leaf(func)
599599
append!(args, compile_args(ctx, ex[1:1]))
@@ -602,18 +602,27 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
602602
end
603603
# 2nd to 5th arguments of foreigncall are special. They must be
604604
# left in place but cannot reference locals.
605-
if contains_nonglobal_binding(ctx, ex[2])
606-
throw(LoweringError(ex[2], "ccall return type cannot reference local variables"))
607-
end
605+
check_no_local_bindings(ctx, ex[2], "ccall return type cannot reference local variables")
608606
for argt in children(ex[3])
609-
if contains_nonglobal_binding(ctx, argt)
610-
throw(LoweringError(argt, "ccall argument types cannot reference local variables"))
611-
end
607+
check_no_local_bindings(ctx, argt,
608+
"ccall argument types cannot reference local variables")
612609
end
613610
append!(args, ex[2:5])
614611
append!(args, compile_args(ctx, ex[6:end]))
615612
args
613+
elseif k == K"cfunction"
614+
# Arguments of cfunction must be left in place except for argument
615+
# 2 (fptr)
616+
args = copy(children(ex))
617+
args[2] = only(compile_args(ctx, args[2:2]))
618+
check_no_local_bindings(ctx, ex[3],
619+
"cfunction return type cannot reference local variables")
620+
for arg in children(ex[4])
621+
check_no_local_bindings(ctx, arg,
622+
"cfunction argument cannot reference local variables")
623+
end
616624
else
625+
# TODO: cglobal
617626
args = compile_args(ctx, children(ex))
618627
end
619628
callex = makenode(ctx, ex, k, args)

src/syntax_macros.jl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,47 @@ function Base.var"@generated"(__context__::MacroContext, ex)
7575
]
7676
end
7777

78+
function Base.var"@cfunction"(__context__::MacroContext, callable, return_type, arg_types)
79+
if kind(arg_types) != K"tuple"
80+
throw(MacroExpansionError(arg_types, "@cfunction argument types must be a literal tuple"))
81+
end
82+
arg_types_svec = @ast __context__ arg_types [K"call"
83+
"svec"::K"core"
84+
children(arg_types)...
85+
]
86+
if kind(callable) == K"$"
87+
fptr = callable[1]
88+
typ = Base.CFunction
89+
else
90+
# Kinda weird semantics here - without `$`, the callable is a top level
91+
# expression which will be evaluated by `jl_resolve_globals_in_ir`,
92+
# implicitly within the module where the `@cfunction` is expanded into.
93+
#
94+
# TODO: The existing flisp implementation is arguably broken because it
95+
# ignores macro hygiene when `callable` is the result of a macro
96+
# expansion within a different module. For now we've inherited this
97+
# brokenness.
98+
#
99+
# Ideally we'd fix this by bringing the scoping rules for this
100+
# expression back into lowering. One option may be to wrap the
101+
# expression in a form which pushes it to top level - maybe as a whole
102+
# separate top level thunk like closure lowering - then use the
103+
# K"captured_local" mechanism to interpolate it back in. This scheme
104+
# would make the complicated scope semantics explicit and let them be
105+
# dealt with in the right place in the frontend rather than putting the
106+
# rules into the runtime itself.
107+
fptr = @ast __context__ callable QuoteNode(Expr(callable))::K"Value"
108+
typ = Ptr{Cvoid}
109+
end
110+
@ast __context__ __context__.macrocall [K"cfunction"
111+
typ::K"Value"
112+
fptr
113+
return_type
114+
arg_types_svec
115+
"ccall"::K"Symbol"
116+
]
117+
end
118+
78119
function Base.GC.var"@preserve"(__context__::MacroContext, exs...)
79120
idents = exs[1:end-1]
80121
for e in idents

test/misc.jl

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let x = [1,2]
1717
GC.@preserve x begin
1818
x
1919
end
20-
end
20+
end
2121
""") == [1,2]
2222

2323
@test JuliaLowering.include_string(test_mod, """
@@ -31,4 +31,19 @@ end
3131
ccall(:strlen, Csize_t, (Cstring,), "asdfg")
3232
""") == 5
3333

34+
# cfunction
35+
JuliaLowering.include_string(test_mod, """
36+
function f_ccallable(x, y)
37+
x + y * 10
38+
end
39+
""")
40+
cf_int = JuliaLowering.include_string(test_mod, """
41+
@cfunction(f_ccallable, Int, (Int,Int))
42+
""")
43+
@test @ccall($cf_int(2::Int, 3::Int)::Int) == 32
44+
cf_float = JuliaLowering.include_string(test_mod, """
45+
@cfunction(f_ccallable, Float64, (Float64,Float64))
46+
""")
47+
@test @ccall($cf_float(2::Float64, 3::Float64)::Float64) == 32.0
48+
3449
end

test/misc_ir.jl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,53 @@ JuxtTest.@emit_juxt
319319
3 (call %10 %₂)
320320
4 (return %₃)
321321

322+
########################################
323+
# @cfunction expansion with global generic function as function argument
324+
@cfunction(callable, Int, (Int, Float64))
325+
#---------------------
326+
1 (cfunction Ptr{Nothing} :(:callable) TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall)
327+
2 (return %₁)
328+
329+
########################################
330+
# @cfunction expansion with closed-over callable argument
331+
@cfunction($close_over, Int, (Int, Float64))
332+
#---------------------
333+
1 TestMod.close_over
334+
2 (cfunction Base.CFunction %₁ TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall)
335+
3 (return %₂)
336+
337+
########################################
338+
# Error: Bad arg types to @cfunction
339+
@cfunction(f, Int, NotATuple)
340+
#---------------------
341+
MacroExpansionError while expanding @cfunction in module Main.TestMod:
342+
@cfunction(f, Int, NotATuple)
343+
# └───────┘ ── @cfunction argument types must be a literal tuple
344+
345+
########################################
346+
# Error: Locals used in @cfunction return type
347+
let T=Float64
348+
@cfunction(f, T, (Float64,))
349+
end
350+
#---------------------
351+
LoweringError:
352+
let T=Float64
353+
@cfunction(f, T, (Float64,))
354+
# ╙ ── cfunction return type cannot reference local variables
355+
end
356+
357+
########################################
358+
# Error: Locals used in @cfunction arg type
359+
let T=Float64
360+
@cfunction(f, Float64, (Float64,T))
361+
end
362+
#---------------------
363+
LoweringError:
364+
let T=Float64
365+
@cfunction(f, Float64, (Float64,T))
366+
# ╙ ── cfunction argument cannot reference local variables
367+
end
368+
322369
########################################
323370
# Error: unary & syntax
324371
&x

0 commit comments

Comments
 (0)