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