Skip to content

Commit bc0ad7f

Browse files
authored
Add K"static_eval" for cfunction/ccall/cglobal (#36)
A few special forms have a kind of "deferred static evaluation" semantics for some of their children: * `@cfunction` - the function name and types * `ccall` / `foreigncall` / `@ccall` - the type arguments and sometimes the function name * `cglobal` - the function name * `@generated` - the expression defining the generated function stub For example, in `@ccall f()::Int`, the `Int` means "the symbol `Int` as looked up in global scope in the module, or as a static parameter of the method", and should fail if `Int` refers to a local variable. Currently all three of these cases are handled through different mechanisms with varying levels of hygiene inconsistency and ability to warn about access to local variables. To fix this problem, introduce the new `K"static_eval"` form which wraps an expression and preserves it as a piece of AST in the output (rather than producing IR), but still resolves scope and hygiene. Use this new form to remove all the special case child-index-dependent handling of these disparate forms from the IR. Also fixes bugs in `Base.@cfunction` hygiene where the function name might be resolved to a global symbol in the wrong module. Also move demo `@ccall` implementation into JuliaLowering, clean up and fix a few things which were broken and implement the gc_safe option from `Base.@ccall`. Makes use of static_eval kind for more precise diagnostics.
1 parent 7badaaa commit bc0ad7f

15 files changed

+442
-228
lines changed

src/ast.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,8 @@ function getmeta(ex::SyntaxTree, name::Symbol, default)
517517
isnothing(meta) ? default : get(meta, name, default)
518518
end
519519

520+
name_hint(name) = CompileHints(:name_hint, name)
521+
520522
#-------------------------------------------------------------------------------
521523
# Predicates and accessors working on expression trees
522524

src/closure_conversion.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ function _convert_closures(ctx::ClosureConversionCtx, ex)
320320
else
321321
access
322322
end
323-
elseif is_leaf(ex) || k == K"inert"
323+
elseif is_leaf(ex) || k == K"inert" || k == K"static_eval"
324324
ex
325325
elseif k == K"="
326326
convert_assignment(ctx, ex)

src/desugaring.jl

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,17 +1626,24 @@ function expand_kw_call(ctx, srcref, farg, args, kws)
16261626
]
16271627
end
16281628

1629+
# Expand the (sym,lib) argument to ccall/cglobal
1630+
function expand_C_library_symbol(ctx, ex)
1631+
expanded = expand_forms_2(ctx, ex)
1632+
if kind(ex) == K"tuple"
1633+
expanded = @ast ctx ex [K"static_eval"(meta=name_hint("function name and library expression"))
1634+
expanded
1635+
]
1636+
end
1637+
return expanded
1638+
end
1639+
16291640
function expand_ccall(ctx, ex)
16301641
@assert kind(ex) == K"call" && is_core_ref(ex[1], "ccall")
16311642
if numchildren(ex) < 4
16321643
throw(LoweringError(ex, "too few arguments to ccall"))
16331644
end
16341645
cfunc_name = ex[2]
16351646
# Detect calling convention if present.
1636-
#
1637-
# Note `@ccall` also emits `Expr(:cconv, convention, nreq)`, but this is a
1638-
# somewhat undocumented performance workaround. Instead we should just make
1639-
# sure @ccall can emit foreigncall directly and efficiently.
16401647
known_conventions = ("cdecl", "stdcall", "fastcall", "thiscall", "llvmcall")
16411648
cconv = if any(is_same_identifier_like(ex[3], id) for id in known_conventions)
16421649
ex[3]
@@ -1748,11 +1755,15 @@ function expand_ccall(ctx, ex)
17481755
@ast ctx ex [K"block"
17491756
sctx.stmts...
17501757
[K"foreigncall"
1751-
expand_forms_2(ctx, cfunc_name)
1752-
expand_forms_2(ctx, return_type)
1753-
[K"call"
1754-
"svec"::K"core"
1755-
expanded_types...
1758+
expand_C_library_symbol(ctx, cfunc_name)
1759+
[K"static_eval"(meta=name_hint("ccall return type"))
1760+
expand_forms_2(ctx, return_type)
1761+
]
1762+
[K"static_eval"(meta=name_hint("ccall argument type"))
1763+
[K"call"
1764+
"svec"::K"core"
1765+
expanded_types...
1766+
]
17561767
]
17571768
num_required_args::K"Integer"
17581769
if isnothing(cconv)
@@ -1828,6 +1839,15 @@ function expand_call(ctx, ex)
18281839
farg = ex[1]
18291840
if is_core_ref(farg, "ccall")
18301841
return expand_ccall(ctx, ex)
1842+
elseif is_core_ref(farg, "cglobal")
1843+
@chk numchildren(ex) in 2:3 (ex, "cglobal must have one or two arguments")
1844+
return @ast ctx ex [K"call"
1845+
ex[1]
1846+
expand_C_library_symbol(ctx, ex[2])
1847+
if numchildren(ex) == 3
1848+
expand_forms_2(ctx, ex[3])
1849+
end
1850+
]
18311851
end
18321852
args = copy(ex[2:end])
18331853
kws = remove_kw_args!(ctx, args)

src/eval.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
302302
# Unpack K"Symbol" QuoteNode as `Expr(:meta)` requires an identifier here.
303303
args[1] = args[1].value
304304
Expr(:meta, args...)
305+
elseif k == K"static_eval"
306+
@assert numchildren(ex) == 1
307+
to_lowered_expr(mod, ex[1], ssa_offset)
308+
elseif k == K"cfunction"
309+
args = Any[to_lowered_expr(mod, e, ssa_offset) for e in children(ex)]
310+
if kind(ex[2]) == K"static_eval"
311+
args[2] = QuoteNode(args[2])
312+
end
313+
Expr(:cfunction, args...)
305314
else
306315
# Allowed forms according to https://docs.julialang.org/en/v1/devdocs/ast/
307316
#
@@ -324,7 +333,6 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
324333
k == K"gc_preserve_begin" ? :gc_preserve_begin :
325334
k == K"gc_preserve_end" ? :gc_preserve_end :
326335
k == K"foreigncall" ? :foreigncall :
327-
k == K"cfunction" ? :cfunction :
328336
k == K"new_opaque_closure" ? :new_opaque_closure :
329337
nothing
330338
if isnothing(head)

src/kinds.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ function _register_kinds()
5151
# For expr-macro compatibility; gone after expansion
5252
"escape"
5353
"hygienic_scope"
54+
# An expression which will eventually be evaluated "statically" in
55+
# the context of a CodeInfo and thus allows access only to globals
56+
# and static parameters. Used for ccall, cfunction, cglobal
57+
# TODO: Use this for GeneratedFunctionStub also?
58+
"static_eval"
5459
# Catch-all for additional syntax extensions without the need to
5560
# extend `Kind`. Known extensions include:
5661
# locals, islocal

src/linear_ir.jl

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
function is_valid_ir_argument(ctx, ex)
55
k = kind(ex)
6-
if is_simple_atom(ctx, ex) || k in KSet"inert top core quote"
6+
if is_simple_atom(ctx, ex) || k in KSet"inert top core quote static_eval"
77
true
88
elseif k == K"BindingId"
99
binfo = lookup_binding(ctx, ex)
@@ -112,7 +112,7 @@ end
112112
function is_simple_arg(ctx, ex)
113113
k = kind(ex)
114114
return is_simple_atom(ctx, ex) || k == K"BindingId" || k == K"quote" || k == K"inert" ||
115-
k == K"top" || k == K"core" || k == K"globalref"
115+
k == K"top" || k == K"core" || k == K"globalref" || k == K"static_eval"
116116
end
117117

118118
function is_single_assign_var(ctx::LinearIRContext, ex)
@@ -128,7 +128,7 @@ function is_const_read_arg(ctx, ex)
128128
# Even if we have side effects, we know that singly-assigned
129129
# locals cannot be affected by them so we can inline them anyway.
130130
# TODO from flisp: "We could also allow const globals here"
131-
return k == K"inert" || k == K"top" || k == K"core" ||
131+
return k == K"inert" || k == K"top" || k == K"core" || k == K"static_eval" ||
132132
is_simple_atom(ctx, ex) || is_single_assign_var(ctx, ex)
133133
end
134134

@@ -167,19 +167,6 @@ function compile_args(ctx, args)
167167
return args_out
168168
end
169169

170-
# Compile the (sym,lib) argument to ccall/cglobal
171-
function compile_C_library_symbol(ctx, ex)
172-
if kind(ex) == K"call" && kind(ex[1]) == K"core" && ex[1].name_val == "tuple"
173-
# Tuples like core.tuple(:funcname, mylib_name) are allowed and are
174-
# kept inline, but may only reference globals.
175-
check_no_local_bindings(ctx, ex,
176-
"function name and library expression cannot reference local variables")
177-
ex
178-
else
179-
only(compile_args(ctx, (ex,)))
180-
end
181-
end
182-
183170
function emit(ctx::LinearIRContext, ex)
184171
push!(ctx.code, ex)
185172
return ex
@@ -593,7 +580,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
593580
k = kind(ex)
594581
if k == K"BindingId" || is_literal(k) || k == K"quote" || k == K"inert" ||
595582
k == K"top" || k == K"core" || k == K"Value" || k == K"Symbol" ||
596-
k == K"SourceLocation"
583+
k == K"SourceLocation" || k == K"static_eval"
597584
if in_tail_pos
598585
emit_return(ctx, ex)
599586
elseif needs_value
@@ -614,39 +601,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
614601
nothing
615602
elseif k == K"call" || k == K"new" || k == K"splatnew" || k == K"foreigncall" ||
616603
k == K"new_opaque_closure" || k == K"cfunction"
617-
if k == K"foreigncall"
618-
args = SyntaxList(ctx)
619-
push!(args, compile_C_library_symbol(ctx, ex[1]))
620-
# 2nd to 5th arguments of foreigncall are special. They must be
621-
# left in place but cannot reference locals.
622-
check_no_local_bindings(ctx, ex[2], "ccall return type cannot reference local variables")
623-
for argt in children(ex[3])
624-
check_no_local_bindings(ctx, argt,
625-
"ccall argument types cannot reference local variables")
626-
end
627-
append!(args, ex[2:5])
628-
append!(args, compile_args(ctx, ex[6:end]))
629-
args
630-
elseif k == K"cfunction"
631-
# Arguments of cfunction must be left in place except for argument
632-
# 2 (fptr)
633-
args = copy(children(ex))
634-
args[2] = only(compile_args(ctx, args[2:2]))
635-
check_no_local_bindings(ctx, ex[3],
636-
"cfunction return type cannot reference local variables")
637-
for arg in children(ex[4])
638-
check_no_local_bindings(ctx, arg,
639-
"cfunction argument cannot reference local variables")
640-
end
641-
elseif k == K"call" && is_core_ref(ex[1], "cglobal")
642-
args = SyntaxList(ctx)
643-
push!(args, ex[1])
644-
push!(args, compile_C_library_symbol(ctx, ex[2]))
645-
append!(args, compile_args(ctx, ex[3:end]))
646-
else
647-
args = compile_args(ctx, children(ex))
648-
end
649-
callex = makenode(ctx, ex, k, args)
604+
callex = makenode(ctx, ex, k, compile_args(ctx, children(ex)))
650605
if in_tail_pos
651606
emit_return(ctx, ex, callex)
652607
elseif needs_value
@@ -909,7 +864,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
909864
end
910865

911866
function _remove_vars_with_isdefined_check!(vars, ex)
912-
if is_leaf(ex) || is_quoted(ex)
867+
if is_leaf(ex) || is_quoted(ex) || kind(ex) == K"static_eval"
913868
return
914869
elseif kind(ex) == K"isdefined"
915870
delete!(vars, ex[1].var_id)
@@ -1017,10 +972,11 @@ function _renumber(ctx, ssa_rewrites, slot_rewrites, label_table, ex)
1017972
makeleaf(ctx, ex, K"globalref", binfo.name, mod=binfo.mod)
1018973
end
1019974
end
1020-
elseif k == K"meta"
975+
elseif k == K"meta" || k == K"static_eval"
1021976
# Somewhat-hack for Expr(:meta, :generated, gen) which has
1022977
# weird top-level semantics for `gen`, but we still need to translate
1023-
# the binding it contains to a globalref.
978+
# the binding it contains to a globalref. (TODO: use
979+
# static_eval for this meta, somehow)
1024980
mapchildren(ctx, ex) do e
1025981
_renumber(ctx, ssa_rewrites, slot_rewrites, label_table, e)
1026982
end

src/macro_expansion.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ function Base.showerror(io::IO, exc::MacroExpansionError)
115115
pos == :end ? (lb+1:lb) :
116116
error("Unknown position $pos")
117117
highlight(io, src.file, byterange, note=exc.msg)
118+
if !isnothing(exc.err)
119+
print(io, "\nCaused by:\n")
120+
showerror(io, exc.err)
121+
end
118122
end
119123

120124
function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::SyntaxTree)
@@ -222,6 +226,9 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree)
222226
if all(==('_'), name_str)
223227
@ast ctx ex ex=>K"Placeholder"
224228
elseif is_ccall_or_cglobal(name_str)
229+
# Lower special identifiers `cglobal` and `ccall` to `K"core"`
230+
# psuedo-refs very early so that cglobal and ccall can never be
231+
# turned into normal bindings (eg, assigned to)
225232
@ast ctx ex name_str::K"core"
226233
else
227234
layerid = get(ex, :scope_layer, ctx.current_layer.id)

src/scope_analysis.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,24 @@ function init_closure_bindings!(ctx, fname)
630630
end
631631
end
632632

633+
function find_any_local_binding(ctx, ex)
634+
k = kind(ex)
635+
if k == K"BindingId"
636+
bkind = lookup_binding(ctx, ex.var_id).kind
637+
if bkind != :global && bkind != :static_parameter
638+
return ex
639+
end
640+
elseif !is_leaf(ex) && !is_quoted(ex)
641+
for e in children(ex)
642+
r = find_any_local_binding(ctx, e)
643+
if !isnothing(r)
644+
return r
645+
end
646+
end
647+
end
648+
return nothing
649+
end
650+
633651
# Update ctx.bindings and ctx.lambda_bindings metadata based on binding usage
634652
function analyze_variables!(ctx, ex)
635653
k = kind(ex)
@@ -649,6 +667,13 @@ function analyze_variables!(ctx, ex)
649667
end
650668
elseif is_leaf(ex) || is_quoted(ex)
651669
return
670+
elseif k == K"static_eval"
671+
badvar = find_any_local_binding(ctx, ex[1])
672+
if !isnothing(badvar)
673+
name_hint = getmeta(ex, :name_hint, "syntax")
674+
throw(LoweringError(badvar, "$(name_hint) cannot reference local variable"))
675+
end
676+
return
652677
elseif k == K"local" || k == K"global"
653678
# Presence of BindingId within local/global is ignored.
654679
return

0 commit comments

Comments
 (0)