From 617970e8b2469a650b53cad7d224335cdd27a142 Mon Sep 17 00:00:00 2001 From: Claire Foster Date: Sat, 16 Aug 2025 21:55:25 +1000 Subject: [PATCH] Add `K"static_eval"` for cfunction/ccall/cglobal 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. --- src/ast.jl | 2 + src/closure_conversion.jl | 2 +- src/desugaring.jl | 38 +++++++--- src/eval.jl | 10 ++- src/kinds.jl | 5 ++ src/linear_ir.jl | 62 +++------------ src/macro_expansion.jl | 7 ++ src/scope_analysis.jl | 25 ++++++ src/syntax_macros.jl | 155 +++++++++++++++++++++++++++++++++----- test/ccall_demo.jl | 129 ------------------------------- test/function_calls_ir.jl | 40 +++++++--- test/macros.jl | 16 +++- test/macros_ir.jl | 4 + test/misc.jl | 53 +++++++++++++ test/misc_ir.jl | 122 +++++++++++++++++++++++++++++- 15 files changed, 442 insertions(+), 228 deletions(-) delete mode 100644 test/ccall_demo.jl diff --git a/src/ast.jl b/src/ast.jl index 16239bf7..bbe59015 100644 --- a/src/ast.jl +++ b/src/ast.jl @@ -517,6 +517,8 @@ function getmeta(ex::SyntaxTree, name::Symbol, default) isnothing(meta) ? default : get(meta, name, default) end +name_hint(name) = CompileHints(:name_hint, name) + #------------------------------------------------------------------------------- # Predicates and accessors working on expression trees diff --git a/src/closure_conversion.jl b/src/closure_conversion.jl index b261cf36..3ac34eff 100644 --- a/src/closure_conversion.jl +++ b/src/closure_conversion.jl @@ -320,7 +320,7 @@ function _convert_closures(ctx::ClosureConversionCtx, ex) else access end - elseif is_leaf(ex) || k == K"inert" + elseif is_leaf(ex) || k == K"inert" || k == K"static_eval" ex elseif k == K"=" convert_assignment(ctx, ex) diff --git a/src/desugaring.jl b/src/desugaring.jl index 33a93647..4a4d4e29 100644 --- a/src/desugaring.jl +++ b/src/desugaring.jl @@ -1626,6 +1626,17 @@ function expand_kw_call(ctx, srcref, farg, args, kws) ] end +# Expand the (sym,lib) argument to ccall/cglobal +function expand_C_library_symbol(ctx, ex) + expanded = expand_forms_2(ctx, ex) + if kind(ex) == K"tuple" + expanded = @ast ctx ex [K"static_eval"(meta=name_hint("function name and library expression")) + expanded + ] + end + return expanded +end + function expand_ccall(ctx, ex) @assert kind(ex) == K"call" && is_core_ref(ex[1], "ccall") if numchildren(ex) < 4 @@ -1633,10 +1644,6 @@ function expand_ccall(ctx, ex) end cfunc_name = ex[2] # Detect calling convention if present. - # - # Note `@ccall` also emits `Expr(:cconv, convention, nreq)`, but this is a - # somewhat undocumented performance workaround. Instead we should just make - # sure @ccall can emit foreigncall directly and efficiently. known_conventions = ("cdecl", "stdcall", "fastcall", "thiscall", "llvmcall") cconv = if any(is_same_identifier_like(ex[3], id) for id in known_conventions) ex[3] @@ -1748,11 +1755,15 @@ function expand_ccall(ctx, ex) @ast ctx ex [K"block" sctx.stmts... [K"foreigncall" - expand_forms_2(ctx, cfunc_name) - expand_forms_2(ctx, return_type) - [K"call" - "svec"::K"core" - expanded_types... + expand_C_library_symbol(ctx, cfunc_name) + [K"static_eval"(meta=name_hint("ccall return type")) + expand_forms_2(ctx, return_type) + ] + [K"static_eval"(meta=name_hint("ccall argument type")) + [K"call" + "svec"::K"core" + expanded_types... + ] ] num_required_args::K"Integer" if isnothing(cconv) @@ -1828,6 +1839,15 @@ function expand_call(ctx, ex) farg = ex[1] if is_core_ref(farg, "ccall") return expand_ccall(ctx, ex) + elseif is_core_ref(farg, "cglobal") + @chk numchildren(ex) in 2:3 (ex, "cglobal must have one or two arguments") + return @ast ctx ex [K"call" + ex[1] + expand_C_library_symbol(ctx, ex[2]) + if numchildren(ex) == 3 + expand_forms_2(ctx, ex[3]) + end + ] end args = copy(ex[2:end]) kws = remove_kw_args!(ctx, args) diff --git a/src/eval.jl b/src/eval.jl index a7896bf0..1f375ab1 100644 --- a/src/eval.jl +++ b/src/eval.jl @@ -302,6 +302,15 @@ function to_lowered_expr(mod, ex, ssa_offset=0) # Unpack K"Symbol" QuoteNode as `Expr(:meta)` requires an identifier here. args[1] = args[1].value Expr(:meta, args...) + elseif k == K"static_eval" + @assert numchildren(ex) == 1 + to_lowered_expr(mod, ex[1], ssa_offset) + elseif k == K"cfunction" + args = Any[to_lowered_expr(mod, e, ssa_offset) for e in children(ex)] + if kind(ex[2]) == K"static_eval" + args[2] = QuoteNode(args[2]) + end + Expr(:cfunction, args...) else # Allowed forms according to https://docs.julialang.org/en/v1/devdocs/ast/ # @@ -324,7 +333,6 @@ function to_lowered_expr(mod, ex, ssa_offset=0) k == K"gc_preserve_begin" ? :gc_preserve_begin : k == K"gc_preserve_end" ? :gc_preserve_end : k == K"foreigncall" ? :foreigncall : - k == K"cfunction" ? :cfunction : k == K"new_opaque_closure" ? :new_opaque_closure : nothing if isnothing(head) diff --git a/src/kinds.jl b/src/kinds.jl index efd73c24..7aa17aa4 100644 --- a/src/kinds.jl +++ b/src/kinds.jl @@ -51,6 +51,11 @@ function _register_kinds() # For expr-macro compatibility; gone after expansion "escape" "hygienic_scope" + # An expression which will eventually be evaluated "statically" in + # the context of a CodeInfo and thus allows access only to globals + # and static parameters. Used for ccall, cfunction, cglobal + # TODO: Use this for GeneratedFunctionStub also? + "static_eval" # Catch-all for additional syntax extensions without the need to # extend `Kind`. Known extensions include: # locals, islocal diff --git a/src/linear_ir.jl b/src/linear_ir.jl index 95700c1e..bf4c7413 100644 --- a/src/linear_ir.jl +++ b/src/linear_ir.jl @@ -3,7 +3,7 @@ function is_valid_ir_argument(ctx, ex) k = kind(ex) - if is_simple_atom(ctx, ex) || k in KSet"inert top core quote" + if is_simple_atom(ctx, ex) || k in KSet"inert top core quote static_eval" true elseif k == K"BindingId" binfo = lookup_binding(ctx, ex) @@ -112,7 +112,7 @@ end function is_simple_arg(ctx, ex) k = kind(ex) return is_simple_atom(ctx, ex) || k == K"BindingId" || k == K"quote" || k == K"inert" || - k == K"top" || k == K"core" || k == K"globalref" + k == K"top" || k == K"core" || k == K"globalref" || k == K"static_eval" end function is_single_assign_var(ctx::LinearIRContext, ex) @@ -128,7 +128,7 @@ function is_const_read_arg(ctx, ex) # Even if we have side effects, we know that singly-assigned # locals cannot be affected by them so we can inline them anyway. # TODO from flisp: "We could also allow const globals here" - return k == K"inert" || k == K"top" || k == K"core" || + return k == K"inert" || k == K"top" || k == K"core" || k == K"static_eval" || is_simple_atom(ctx, ex) || is_single_assign_var(ctx, ex) end @@ -167,19 +167,6 @@ function compile_args(ctx, args) return args_out end -# Compile the (sym,lib) argument to ccall/cglobal -function compile_C_library_symbol(ctx, ex) - if kind(ex) == K"call" && kind(ex[1]) == K"core" && ex[1].name_val == "tuple" - # Tuples like core.tuple(:funcname, mylib_name) are allowed and are - # kept inline, but may only reference globals. - check_no_local_bindings(ctx, ex, - "function name and library expression cannot reference local variables") - ex - else - only(compile_args(ctx, (ex,))) - end -end - function emit(ctx::LinearIRContext, ex) push!(ctx.code, ex) return ex @@ -593,7 +580,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos) k = kind(ex) if k == K"BindingId" || is_literal(k) || k == K"quote" || k == K"inert" || k == K"top" || k == K"core" || k == K"Value" || k == K"Symbol" || - k == K"SourceLocation" + k == K"SourceLocation" || k == K"static_eval" if in_tail_pos emit_return(ctx, ex) elseif needs_value @@ -614,39 +601,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos) nothing elseif k == K"call" || k == K"new" || k == K"splatnew" || k == K"foreigncall" || k == K"new_opaque_closure" || k == K"cfunction" - if k == K"foreigncall" - args = SyntaxList(ctx) - push!(args, compile_C_library_symbol(ctx, ex[1])) - # 2nd to 5th arguments of foreigncall are special. They must be - # left in place but cannot reference locals. - check_no_local_bindings(ctx, ex[2], "ccall return type cannot reference local variables") - for argt in children(ex[3]) - check_no_local_bindings(ctx, argt, - "ccall argument types cannot reference local variables") - end - append!(args, ex[2:5]) - append!(args, compile_args(ctx, ex[6:end])) - args - elseif k == K"cfunction" - # Arguments of cfunction must be left in place except for argument - # 2 (fptr) - args = copy(children(ex)) - args[2] = only(compile_args(ctx, args[2:2])) - check_no_local_bindings(ctx, ex[3], - "cfunction return type cannot reference local variables") - for arg in children(ex[4]) - check_no_local_bindings(ctx, arg, - "cfunction argument cannot reference local variables") - end - elseif k == K"call" && is_core_ref(ex[1], "cglobal") - args = SyntaxList(ctx) - push!(args, ex[1]) - push!(args, compile_C_library_symbol(ctx, ex[2])) - append!(args, compile_args(ctx, ex[3:end])) - else - args = compile_args(ctx, children(ex)) - end - callex = makenode(ctx, ex, k, args) + callex = makenode(ctx, ex, k, compile_args(ctx, children(ex))) if in_tail_pos emit_return(ctx, ex, callex) elseif needs_value @@ -909,7 +864,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos) end function _remove_vars_with_isdefined_check!(vars, ex) - if is_leaf(ex) || is_quoted(ex) + if is_leaf(ex) || is_quoted(ex) || kind(ex) == K"static_eval" return elseif kind(ex) == K"isdefined" delete!(vars, ex[1].var_id) @@ -1017,10 +972,11 @@ function _renumber(ctx, ssa_rewrites, slot_rewrites, label_table, ex) makeleaf(ctx, ex, K"globalref", binfo.name, mod=binfo.mod) end end - elseif k == K"meta" + elseif k == K"meta" || k == K"static_eval" # Somewhat-hack for Expr(:meta, :generated, gen) which has # weird top-level semantics for `gen`, but we still need to translate - # the binding it contains to a globalref. + # the binding it contains to a globalref. (TODO: use + # static_eval for this meta, somehow) mapchildren(ctx, ex) do e _renumber(ctx, ssa_rewrites, slot_rewrites, label_table, e) end diff --git a/src/macro_expansion.jl b/src/macro_expansion.jl index d3abadde..f02f2fa8 100644 --- a/src/macro_expansion.jl +++ b/src/macro_expansion.jl @@ -115,6 +115,10 @@ function Base.showerror(io::IO, exc::MacroExpansionError) pos == :end ? (lb+1:lb) : error("Unknown position $pos") highlight(io, src.file, byterange, note=exc.msg) + if !isnothing(exc.err) + print(io, "\nCaused by:\n") + showerror(io, exc.err) + end end function eval_macro_name(ctx::MacroExpansionContext, mctx::MacroContext, ex::SyntaxTree) @@ -222,6 +226,9 @@ function expand_forms_1(ctx::MacroExpansionContext, ex::SyntaxTree) if all(==('_'), name_str) @ast ctx ex ex=>K"Placeholder" elseif is_ccall_or_cglobal(name_str) + # Lower special identifiers `cglobal` and `ccall` to `K"core"` + # psuedo-refs very early so that cglobal and ccall can never be + # turned into normal bindings (eg, assigned to) @ast ctx ex name_str::K"core" else layerid = get(ex, :scope_layer, ctx.current_layer.id) diff --git a/src/scope_analysis.jl b/src/scope_analysis.jl index c0f65953..8c755462 100644 --- a/src/scope_analysis.jl +++ b/src/scope_analysis.jl @@ -630,6 +630,24 @@ function init_closure_bindings!(ctx, fname) end end +function find_any_local_binding(ctx, ex) + k = kind(ex) + if k == K"BindingId" + bkind = lookup_binding(ctx, ex.var_id).kind + if bkind != :global && bkind != :static_parameter + return ex + end + elseif !is_leaf(ex) && !is_quoted(ex) + for e in children(ex) + r = find_any_local_binding(ctx, e) + if !isnothing(r) + return r + end + end + end + return nothing +end + # Update ctx.bindings and ctx.lambda_bindings metadata based on binding usage function analyze_variables!(ctx, ex) k = kind(ex) @@ -649,6 +667,13 @@ function analyze_variables!(ctx, ex) end elseif is_leaf(ex) || is_quoted(ex) return + elseif k == K"static_eval" + badvar = find_any_local_binding(ctx, ex[1]) + if !isnothing(badvar) + name_hint = getmeta(ex, :name_hint, "syntax") + throw(LoweringError(badvar, "$(name_hint) cannot reference local variable")) + end + return elseif k == K"local" || k == K"global" # Presence of BindingId within local/global is ignored. return diff --git a/src/syntax_macros.jl b/src/syntax_macros.jl index d9fac7be..43c186cd 100644 --- a/src/syntax_macros.jl +++ b/src/syntax_macros.jl @@ -91,32 +91,153 @@ function Base.var"@cfunction"(__context__::MacroContext, callable, return_type, # Kinda weird semantics here - without `$`, the callable is a top level # expression which will be evaluated by `jl_resolve_globals_in_ir`, # implicitly within the module where the `@cfunction` is expanded into. - # - # TODO: The existing flisp implementation is arguably broken because it - # ignores macro hygiene when `callable` is the result of a macro - # expansion within a different module. For now we've inherited this - # brokenness. - # - # Ideally we'd fix this by bringing the scoping rules for this - # expression back into lowering. One option may be to wrap the - # expression in a form which pushes it to top level - maybe as a whole - # separate top level thunk like closure lowering - then use the - # K"captured_local" mechanism to interpolate it back in. This scheme - # would make the complicated scope semantics explicit and let them be - # dealt with in the right place in the frontend rather than putting the - # rules into the runtime itself. - fptr = @ast __context__ callable QuoteNode(Expr(callable))::K"Value" + fptr = @ast __context__ callable [K"static_eval"( + meta=name_hint("cfunction function name")) + callable + ] typ = Ptr{Cvoid} end @ast __context__ __context__.macrocall [K"cfunction" typ::K"Value" fptr - return_type - arg_types_svec + [K"static_eval"(meta=name_hint("cfunction return type")) + return_type + ] + [K"static_eval"(meta=name_hint("cfunction argument type")) + arg_types_svec + ] "ccall"::K"Symbol" ] end +function ccall_macro_parse(ctx, ex, opts) + gc_safe=false + for opt in opts + if kind(opt) != K"=" || numchildren(opt) != 2 || + kind(opt[1]) != K"Identifier" + throw(MacroExpansionError(opt, "Bad option to ccall")) + else + optname = opt[1].name_val + if optname == "gc_safe" + if kind(opt[2]) == K"Bool" + gc_safe = opt[2].value::Bool + else + throw(MacroExpansionError(opt[2], "gc_safe must be true or false")) + end + else + throw(MacroExpansionError(opt[1], "Unknown option name for ccall")) + end + end + end + + if kind(ex) != K"::" + throw(MacroExpansionError(ex, "Expected a return type annotation `::SomeType`", position=:end)) + end + + rettype = ex[2] + call = ex[1] + if kind(call) != K"call" + throw(MacroExpansionError(call, "Expected function call syntax `f()`")) + end + + func = call[1] + varargs = numchildren(call) > 1 && kind(call[end]) == K"parameters" ? + children(call[end]) : nothing + + # collect args and types + args = SyntaxList(ctx) + types = SyntaxList(ctx) + function pusharg!(arg) + if kind(arg) != K"::" + throw(MacroExpansionError(arg, "argument needs a type annotation")) + end + push!(args, arg[1]) + push!(types, arg[2]) + end + + for e in call[2:(isnothing(varargs) ? end : end-1)] + kind(e) != K"parameters" || throw(MacroExpansionError(call[end], "Multiple parameter blocks not allowed")) + pusharg!(e) + end + + if !isnothing(varargs) + num_required_args = length(args) + if num_required_args == 0 + throw(MacroExpansionError(call[end], "C ABI prohibits varargs without one required argument")) + end + for e in varargs + pusharg!(e) + end + else + num_required_args = 0 # Non-vararg call + end + + return func, rettype, types, args, gc_safe, num_required_args +end + +function ccall_macro_lower(ctx, ex, convention, func, rettype, types, args, gc_safe, num_required_args) + statements = SyntaxTree[] + kf = kind(func) + if kf == K"Identifier" + lowered_func = @ast ctx func func=>K"Symbol" + elseif kf == K"." + lowered_func = @ast ctx func [K"tuple" + func[2]=>K"Symbol" + [K"static_eval"(meta=name_hint("@ccall library name")) + func[1] + ] + ] + elseif kf == K"$" + check = @SyntaxTree quote + func = $(func[1]) + if !isa(func, Ptr{Cvoid}) + name = :($(func[1])) + throw(ArgumentError("interpolated function `$name` was not a `Ptr{Cvoid}`, but $(typeof(func))")) + end + end + push!(statements, check) + lowered_func = check[1][1] + else + throw(MacroExpansionError(func, + "Function name must be a symbol like `foo`, a library and function name like `libc.printf` or an interpolated function pointer like `\$ptr`")) + end + + roots = SyntaxTree[] + cargs = SyntaxTree[] + for (i, (type, arg)) in enumerate(zip(types, args)) + argi = @ast ctx arg "arg$i"::K"Identifier" + # TODO: Does it help to emit ssavar() here for the `argi`? + push!(statements, @SyntaxTree :(local $argi = Base.cconvert($type, $arg))) + push!(roots, argi) + push!(cargs, @SyntaxTree :(Base.unsafe_convert($type, $argi))) + end + effect_flags = UInt16(0) + push!(statements, @ast ctx ex [K"foreigncall" + lowered_func + [K"static_eval"(meta=name_hint("@ccall return type")) + rettype + ] + [K"static_eval"(meta=name_hint("@ccall argument type")) + [K"call" + "svec"::K"core" + types... + ] + ] + num_required_args::K"Integer" + QuoteNode((convention, effect_flags, gc_safe))::K"Value" + cargs... + roots... + ]) + + @ast ctx ex [K"block" + statements... + ] +end + +function Base.var"@ccall"(ctx::MacroContext, ex, opts...) + ccall_macro_lower(ctx, ex, :ccall, ccall_macro_parse(ctx, ex, opts)...) +end + function Base.GC.var"@preserve"(__context__::MacroContext, exs...) idents = exs[1:end-1] for e in idents diff --git a/test/ccall_demo.jl b/test/ccall_demo.jl deleted file mode 100644 index f5e2e987..00000000 --- a/test/ccall_demo.jl +++ /dev/null @@ -1,129 +0,0 @@ -module CCall - -using JuliaSyntax, JuliaLowering -using JuliaLowering: is_identifier_like, numchildren, children, MacroExpansionError, @ast, SyntaxTree - -# Hacky utils -# macro K_str(str) -# JuliaSyntax.Kind(str[1].value) -# end -# -# # Needed because we can't lower kwarg calls yet ehehe :-/ -# function mac_ex_error(ex, msg, pos) -# kwargs = Core.apply_type(Core.NamedTuple, (:position,))((pos,)) -# Core.kwcall(kwargs, MacroExpansionError, ex, msg) -# end - -macro ast_str(str) - ex = parsestmt(JuliaLowering.SyntaxTree, str, filename=string(__source__.file)) - ctx1, ex1 = JuliaLowering.expand_forms_1(__module__, ex) - @assert kind(ex1) == K"call" && ex1[1].value === JuliaLowering.interpolate_ast - cs = map(e->esc(Expr(e)), ex1[3:end]) - :(JuliaLowering.interpolate_ast($(ex1[2][1]), $(cs...))) -end - -function ccall_macro_parse(ex) - if kind(ex) != K"::" - throw(MacroExpansionError(ex, "Expected a return type annotation like `::T`", position=:end)) - end - - rettype = ex[2] - call = ex[1] - if kind(call) != K"call" - throw(MacroExpansionError(call, "Expected function call syntax `f()`")) - end - - # get the function symbols - func = let f = call[1], kf = kind(f) - if kf == K"." - @ast ex ex [K"tuple" f[2]=>K"Symbol" f[1]] - elseif kf == K"$" - f - elseif kf == K"Identifier" - @ast ex ex f=>K"Symbol" - else - throw(MacroExpansionError(f, - "Function name must be a symbol like `foo`, a library and function name like `libc.printf` or an interpolated function pointer like `\$ptr`")) - end - end - - varargs = nothing - - # collect args and types - args = SyntaxTree[] - types = SyntaxTree[] - - function pusharg!(arg) - if kind(arg) != K"::" - throw(MacroExpansionError(arg, "argument needs a type annotation like `::T`")) - end - push!(args, arg[1]) - push!(types, arg[2]) - end - - varargs = nothing - num_varargs = 0 - for e in call[2:end] - if kind(e) == K"parameters" - num_varargs == 0 || throw(MacroExpansionError(e, "Multiple parameter blocks not allowed")) - num_varargs = numchildren(e) - num_varargs > 0 || throw(MacroExpansionError(e, "C ABI prohibits vararg without one required argument")) - varargs = children(e) - else - pusharg!(e) - end - end - if !isnothing(varargs) - for e in varargs - pusharg!(e) - end - end - - return func, rettype, types, args, num_varargs -end - -function ccall_macro_lower(ex, convention, func, rettype, types, args, num_varargs) - statements = SyntaxTree[] - if kind(func) == K"$" - check = ast"""quote - func = $(func[1]) - if !isa(func, Ptr{Cvoid}) - name = :($(func[1])) - throw(ArgumentError("interpolated function `$name` was not a `Ptr{Cvoid}`, but $(typeof(func))")) - end - end""" - func = check[1][1] - push!(statements, check) - end - - roots = SyntaxTree[] - cargs = SyntaxTree[] - for (i, (type, arg)) in enumerate(zip(types, args)) - argi = @ast ex arg "arg$i"::K"Identifier" - # TODO: Is there any safe way to use SSAValue here? - push!(statements, ast":(local $argi = Base.cconvert($type, $arg))") - push!(roots, argi) - push!(cargs, ast":(Base.unsafe_convert($type, $argi))") - end - push!(statements, - @ast ex ex [K"foreigncall" - func - rettype - ast":(Core.svec($(types...)))" - # Is this num_varargs correct? It seems wrong? - num_varargs::K"Integer" - convention::K"Symbol" - cargs... - roots... - ]) - - @ast ex ex [K"block" - statements... - ] -end - -function var"@ccall"(ctx::JuliaLowering.MacroContext, ex) - ccall_macro_lower(ex, "ccall", ccall_macro_parse(ex)...) -end - -end # module CCall diff --git a/test/function_calls_ir.jl b/test/function_calls_ir.jl index 7017902b..b0a70160 100644 --- a/test/function_calls_ir.jl +++ b/test/function_calls_ir.jl @@ -360,7 +360,7 @@ ccall(:strlen, Csize_t, (Cstring,), "asdfg") 1 TestMod.Cstring 2 (call top.cconvert %₁ "asdfg") 3 (call top.unsafe_convert %₁ %₂) -4 (foreigncall :strlen TestMod.Csize_t (call core.svec TestMod.Cstring) 0 :ccall %₃ %₂) +4 (foreigncall :strlen (static_eval TestMod.Csize_t) (static_eval (call core.svec TestMod.Cstring)) 0 :ccall %₃ %₂) 5 (return %₄) ######################################## @@ -370,14 +370,14 @@ ccall((:strlen, libc), Csize_t, (Cstring,), "asdfg") 1 TestMod.Cstring 2 (call top.cconvert %₁ "asdfg") 3 (call top.unsafe_convert %₁ %₂) -4 (foreigncall (call core.tuple :strlen TestMod.libc) TestMod.Csize_t (call core.svec TestMod.Cstring) 0 :ccall %₃ %₂) +4 (foreigncall (static_eval (call core.tuple :strlen TestMod.libc)) (static_eval TestMod.Csize_t) (static_eval (call core.svec TestMod.Cstring)) 0 :ccall %₃ %₂) 5 (return %₄) ######################################## # ccall with a calling convention ccall(:foo, stdcall, Csize_t, ()) #--------------------- -1 (foreigncall :foo TestMod.Csize_t (call core.svec) 0 :stdcall) +1 (foreigncall :foo (static_eval TestMod.Csize_t) (static_eval (call core.svec)) 0 :stdcall) 2 (return %₁) ######################################## @@ -386,7 +386,7 @@ ccall(:foo, stdcall, Csize_t, (Any,), x) #--------------------- 1 core.Any 2 TestMod.x -3 (foreigncall :foo TestMod.Csize_t (call core.svec core.Any) 0 :stdcall %₂) +3 (foreigncall :foo (static_eval TestMod.Csize_t) (static_eval (call core.svec core.Any)) 0 :stdcall %₂) 4 (return %₃) ######################################## @@ -397,7 +397,7 @@ ccall(ptr, Csize_t, (Cstring,), "asdfg") 2 (call top.cconvert %₁ "asdfg") 3 TestMod.ptr 4 (call top.unsafe_convert %₁ %₂) -5 (foreigncall %₃ TestMod.Csize_t (call core.svec TestMod.Cstring) 0 :ccall %₄ %₂) +5 (foreigncall %₃ (static_eval TestMod.Csize_t) (static_eval (call core.svec TestMod.Cstring)) 0 :ccall %₄ %₂) 6 (return %₅) ######################################## @@ -412,7 +412,7 @@ ccall(:printf, Cint, (Cstring, Cstring...), "%s = %s\n", "2 + 2", "5") 6 (call top.unsafe_convert %₁ %₃) 7 (call top.unsafe_convert %₂ %₄) 8 (call top.unsafe_convert %₂ %₅) -9 (foreigncall :printf TestMod.Cint (call core.svec TestMod.Cstring TestMod.Cstring TestMod.Cstring) 1 :ccall %₆ %₇ %₈ %₃ %₄ %₅) +9 (foreigncall :printf (static_eval TestMod.Cint) (static_eval (call core.svec TestMod.Cstring TestMod.Cstring TestMod.Cstring)) 1 :ccall %₆ %₇ %₈ %₃ %₄ %₅) 10 (return %₉) ######################################## @@ -456,7 +456,7 @@ end LoweringError: let libc = "libc" ccall((:strlen, libc), Csize_t, (Cstring,), "asdfg") -# └─────────────┘ ── function name and library expression cannot reference local variables +# └──┘ ── function name and library expression cannot reference local variable end ######################################## @@ -468,7 +468,7 @@ end LoweringError: let Csize_t = 1 ccall(:strlen, Csize_t, (Cstring,), "asdfg") -# └─────┘ ── ccall return type cannot reference local variables +# └─────┘ ── ccall return type cannot reference local variable end ######################################## @@ -480,7 +480,7 @@ end LoweringError: let Cstring = 1 ccall(:strlen, Csize_t, (Cstring,), "asdfg") -# └─────┘ ── ccall argument types cannot reference local variables +# └─────┘ ── ccall argument type cannot reference local variable end ######################################## @@ -520,7 +520,7 @@ ccall(:foo, Csize_t, (Cstring..., Cstring...), "asdfg", "blah") cglobal((:sym, lib), Int) #--------------------- 1 TestMod.Int -2 (call core.cglobal (call core.tuple :sym TestMod.lib) %₁) +2 (call core.cglobal (static_eval (call core.tuple :sym TestMod.lib)) %₁) 3 (return %₂) ######################################## @@ -533,6 +533,26 @@ cglobal(f(), Int) 4 (call core.cglobal %₂ %₃) 5 (return %₄) +######################################## +# Error: cglobal with library name referencing local variable +let func="myfunc" + cglobal((func, "somelib"), Int) +end +#--------------------- +LoweringError: +let func="myfunc" + cglobal((func, "somelib"), Int) +# └──┘ ── function name and library expression cannot reference local variable +end + +######################################## +# Error: cglobal too many arguments +cglobal(:sym, Int, blah) +#--------------------- +LoweringError: +cglobal(:sym, Int, blah) +└──────────────────────┘ ── cglobal must have one or two arguments + ######################################## # Error: assigning to `cglobal` cglobal = 10 diff --git a/test/macros.jl b/test/macros.jl index 9cfdef95..a75d0fec 100644 --- a/test/macros.jl +++ b/test/macros.jl @@ -172,15 +172,23 @@ let err = try @test err.err isa UndefVarError end -include("ccall_demo.jl") -@test JuliaLowering.include_string(CCall, "@ccall strlen(\"foo\"::Cstring)::Csize_t") == 3 +@test JuliaLowering.include_string(test_mod, "@ccall strlen(\"foo\"::Cstring)::Csize_t") == 3 +@test JuliaLowering.include_string(test_mod, "@ccall strlen(\"asdf\"::Cstring)::Csize_t gc_safe=true") == 4 +@test JuliaLowering.include_string(test_mod, """ +begin + buf = zeros(UInt8, 20) + @ccall sprintf(buf::Ptr{UInt8}, "num:%d str:%s"::Cstring; 42::Cint, "hello"::Cstring)::Cint + String(buf) +end +""") == "num:42 str:hello\0\0\0\0" + let (err, st) = try - JuliaLowering.include_string(CCall, "@ccall strlen(\"foo\"::Cstring)") + JuliaLowering.include_string(test_mod, "@ccall strlen(\"foo\"::Cstring)") catch e e, stacktrace(catch_backtrace()) end @test err isa JuliaLowering.MacroExpansionError - @test err.msg == "Expected a return type annotation like `::T`" + @test err.msg == "Expected a return type annotation `::SomeType`" @test isnothing(err.err) # Check that `catch_backtrace` can capture the stacktrace of the macro function @test any(sf->sf.func===:ccall_macro_parse, st) diff --git a/test/macros_ir.jl b/test/macros_ir.jl index eccefb44..29f4e650 100644 --- a/test/macros_ir.jl +++ b/test/macros_ir.jl @@ -132,3 +132,7 @@ _never_exist = @m_not_exist 42 MacroExpansionError while expanding @m_not_exist in module Main.TestMod: _never_exist = @m_not_exist 42 # └─────────┘ ── Macro not found +Caused by: +UndefVarError: `@m_not_exist` not defined in `Main.TestMod` +Suggestion: check for spelling errors or missing imports. + diff --git a/test/misc.jl b/test/misc.jl index a56923fc..1fa6a15c 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -46,6 +46,59 @@ cf_float = JuliaLowering.include_string(test_mod, """ """) @test @ccall($cf_float(2::Float64, 3::Float64)::Float64) == 32.0 +# Test that hygiene works with @ccallable function names (this is broken in +# Base) +JuliaLowering.include_string(test_mod, raw""" +f_ccallable_hygiene() = 1 + +module Nested + f_ccallable_hygiene() = 2 + macro cfunction_hygiene() + :(@cfunction(f_ccallable_hygiene, Int, ())) + end +end +""") +cf_hygiene = JuliaLowering.include_string(test_mod, """ +Nested.@cfunction_hygiene +""") +@test @ccall($cf_hygiene()::Int) == 2 + +# Test that ccall can be passed static parameters in type signatures. +# +# Note that the cases where this works are extremely limited and tend to look +# like `Ptr{T}` or `Ref{T}` (`T` doesn't work!?) because of the compilation +# order in which the runtime inspects the arguments to ccall (`Ptr{T}` has a +# well defined C ABI even when `T` is not yet determined). See also +# https://github.com/JuliaLang/julia/issues/29400 +# https://github.com/JuliaLang/julia/pull/40947 +JuliaLowering.include_string(test_mod, raw""" +function sparam_ccallable(x::Ptr{T}) where {T} + unsafe_store!(x, one(T)) + nothing +end + +function ccall_with_sparams(::Type{T}) where {T} + x = T[zero(T)] + cf = @cfunction(sparam_ccallable, Cvoid, (Ptr{T},)) + @ccall $cf(x::Ptr{T})::Cvoid + x[1] +end +""") +@test test_mod.ccall_with_sparams(Int) === 1 +@test test_mod.ccall_with_sparams(Float64) === 1.0 + +# Test that ccall can be passed static parameters in the function name +JuliaLowering.include_string(test_mod, raw""" +# In principle, may add other strlen-like functions here for different string +# types +ccallable_sptest_name(::Type{String}) = :strlen + +function ccall_with_sparams_in_name(s::T) where {T} + ccall(ccallable_sptest_name(T), Csize_t, (Cstring,), s) +end +""") +@test test_mod.ccall_with_sparams_in_name("hii") == 3 + @testset "CodeInfo: has_image_globalref" begin @test lower_str(test_mod, "x + y").args[1].has_image_globalref === false @test lower_str(Main, "x + y").args[1].has_image_globalref === true diff --git a/test/misc_ir.jl b/test/misc_ir.jl index 15fa0229..33fe6449 100644 --- a/test/misc_ir.jl +++ b/test/misc_ir.jl @@ -323,7 +323,7 @@ JuxtTest.@emit_juxt # @cfunction expansion with global generic function as function argument @cfunction(callable, Int, (Int, Float64)) #--------------------- -1 (cfunction Ptr{Nothing} :(:callable) TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall) +1 (cfunction Ptr{Nothing} (static_eval TestMod.callable) (static_eval TestMod.Int) (static_eval (call core.svec TestMod.Int TestMod.Float64)) :ccall) 2 (return %₁) ######################################## @@ -331,7 +331,7 @@ JuxtTest.@emit_juxt @cfunction($close_over, Int, (Int, Float64)) #--------------------- 1 TestMod.close_over -2 (cfunction Base.CFunction %₁ TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall) +2 (cfunction Base.CFunction %₁ (static_eval TestMod.Int) (static_eval (call core.svec TestMod.Int TestMod.Float64)) :ccall) 3 (return %₂) ######################################## @@ -351,7 +351,7 @@ end LoweringError: let T=Float64 @cfunction(f, T, (Float64,)) -# ╙ ── cfunction return type cannot reference local variables +# ╙ ── cfunction return type cannot reference local variable end ######################################## @@ -363,9 +363,123 @@ end LoweringError: let T=Float64 @cfunction(f, Float64, (Float64,T)) -# ╙ ── cfunction argument cannot reference local variables +# ╙ ── cfunction argument type cannot reference local variable end +######################################## +# Basic @ccall lowering +@ccall foo(x::X, y::Y)::R +#--------------------- +1 JuliaLowering.Base +2 (call top.getproperty %₁ :cconvert) +3 TestMod.X +4 TestMod.x +5 (= slot₁/arg1 (call %₂ %₃ %₄)) +6 JuliaLowering.Base +7 (call top.getproperty %₆ :cconvert) +8 TestMod.Y +9 TestMod.y +10 (= slot₂/arg2 (call %₇ %₈ %₉)) +11 JuliaLowering.Base +12 (call top.getproperty %₁₁ :unsafe_convert) +13 TestMod.X +14 slot₁/arg1 +15 (call %₁₂ %₁₃ %₁₄) +16 JuliaLowering.Base +17 (call top.getproperty %₁₆ :unsafe_convert) +18 TestMod.Y +19 slot₂/arg2 +20 (call %₁₇ %₁₈ %₁₉) +21 slot₁/arg1 +22 slot₂/arg2 +23 (foreigncall :foo (static_eval TestMod.R) (static_eval (call core.svec TestMod.X TestMod.Y)) 0 :($(QuoteNode((:ccall, 0x0000, false)))) %₁₅ %₂₀ %₂₁ %₂₂) +24 (return %₂₃) + +######################################## +# @ccall lowering with varargs and gc_safe +@ccall foo(x::X; y::Y)::R gc_safe=true +#--------------------- +1 JuliaLowering.Base +2 (call top.getproperty %₁ :cconvert) +3 TestMod.X +4 TestMod.x +5 (= slot₁/arg1 (call %₂ %₃ %₄)) +6 JuliaLowering.Base +7 (call top.getproperty %₆ :cconvert) +8 TestMod.Y +9 TestMod.y +10 (= slot₂/arg2 (call %₇ %₈ %₉)) +11 JuliaLowering.Base +12 (call top.getproperty %₁₁ :unsafe_convert) +13 TestMod.X +14 slot₁/arg1 +15 (call %₁₂ %₁₃ %₁₄) +16 JuliaLowering.Base +17 (call top.getproperty %₁₆ :unsafe_convert) +18 TestMod.Y +19 slot₂/arg2 +20 (call %₁₇ %₁₈ %₁₉) +21 slot₁/arg1 +22 slot₂/arg2 +23 (foreigncall :foo (static_eval TestMod.R) (static_eval (call core.svec TestMod.X TestMod.Y)) 1 :($(QuoteNode((:ccall, 0x0000, true)))) %₁₅ %₂₀ %₂₁ %₂₂) +24 (return %₂₃) + +######################################## +# Error: No return annotation on @ccall +@ccall strlen("foo"::Cstring) +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall strlen("foo"::Cstring) +# └ ── Expected a return type annotation `::SomeType` + +######################################## +# Error: No argument type on @ccall +@ccall foo("blah"::Cstring, "bad")::Int +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo("blah"::Cstring, "bad")::Int +# └───┘ ── argument needs a type annotation + +######################################## +# Error: @ccall varags without one fixed argument +@ccall foo(; x::Int)::Int +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo(; x::Int)::Int +# └──────┘ ── C ABI prohibits varargs without one required argument + +######################################## +# Error: Multiple varargs blocks +@ccall foo(; x::Int; y::Float64)::Int +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo(; x::Int; y::Float64)::Int +# └──────────┘ ── Multiple parameter blocks not allowed + +######################################## +# Error: Bad @ccall option +@ccall foo(x::Int)::Int bad_opt +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo(x::Int)::Int bad_opt +# └─────┘ ── Bad option to ccall + +######################################## +# Error: Unknown @ccall option name +@ccall foo(x::Int)::Int bad_opt=true +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo(x::Int)::Int bad_opt=true +# └─────┘ ── Unknown option name for ccall + +######################################## +# Error: Unknown option type +@ccall foo(x::Int)::Int gc_safe="hi" +#--------------------- +MacroExpansionError while expanding @ccall in module Main.TestMod: +@ccall foo(x::Int)::Int gc_safe="hi" +# └──┘ ── gc_safe must be true or false + ######################################## # Error: unary & syntax &x