Julia integration
LLVM.jl offers several tools to interoperate with Julia's LLVM-based code generator. These tools are part of the Interop
submodule, and need to be imported explicitly.
Type conversion
Julia types can be converted to their LLVM counterparts using the convert
function.
julia> convert(LLVMType, Int64)
i64
By default, this conversion rejects boxed types, as the result would often be surprising. It is possible to query whether a type would be boxed using the isboxed
function, and/or allow the conversion by passing allow_boxed=true
to convert
:
julia> isboxed(Int64)
false
julia> isboxed(String)
true
julia> convert(LLVMType, String; allow_boxed=true)
{} addrspace(10)*
Another useful query is isghosttype
, which returns whether a type is a ghost type, i.e., a sizeless type:
julia> isghosttype(Nothing)
true
julia> isghosttype(LLVM.VoidType())
true
Generated IR functions
With generated functions, it is possible to manually generate Julia IR when a function is visited by the Julia compiler. LLVM.jl extends this with the ability to define functions that generate LLVM IR; which is very useful to generate code that is not easily expressible in Julia:
@generated function add(x::T, y::T) where {T}
@dispose ctx=Context() begin
# get the element type
eltyp = convert(LLVMType, T)
# create a function
paramtyps = [eltyp, eltyp]
f, ft = create_function(eltyp, paramtyps)
# generate IR
@dispose builder=IRBuilder() begin
entry = BasicBlock(f, "entry")
position!(builder, entry)
val = add!(builder, parameters(f)[1], parameters(f)[2])
ret!(builder, val)
end
call_function(f, T, Tuple{T, T}, :x, :y)
end
end
@code_llvm debuginfo=:none add(1, 2)
add(1,2)
# output
; Function Signature: add(Int64, Int64)
define i64 @julia_add_3944(i64 signext %"x::Int64", i64 signext %"y::Int64") #0 {
top:
%0 = add i64 %"y::Int64", %"x::Int64"
ret i64 %0
}
3
The call_function
is where the magic happens: it generates LLVM IR for the function that's being called, and embeds it in the generated Julia IR so that it can be processed by the Julia compiler.
Inline assembly
An extension of this mechanism is the @asmcall
macro, which allows embedding inline assembly in Julia functions:
julia> add(x::Int, y::Int) = @asmcall("add \$0, \$1, \$2", "=r,r,r",
Int64, Tuple{Int64,Int64},
x, y);
julia> add(1,2)
3
julia> @code_native add(1,2)
; InlineAsm Start
add x0, x0, x1
; InlineAsm End
ret
LLVM pointers
Julia's pointer type Ptr
only keeps track of the element type, and not the address space. Julia has Core.LLVMPtr
to track address space information, with the necessary codegen support, but no utility functions. LLVM.jl provides the functionality that's commonly needed when working with pointers:
pointerref
: get the value of memory, at a specific index, with specific alignmentpointerset
: set the value of memory, at a specific index, with specific alignmentunsafe_load
andunsafe_store!
: higher-level versions of these functions- basic pointer arithmetic: conversions to/from integers, addition, subtraction, comparison, etc.
addrspacecast
: convert between pointers with different address spaces
julia> a = [1];
julia> ptr = Core.LLVMPtr{Int,0}(pointer(a));
julia> unsafe_load(ptr)
1
julia> unsafe_store!(ptr, 42)
julia> a
1-element Vector{Int64}:
42
Intrinsics
LLVM.jl also provides Julia functions for common intrinsics, allowing them to be used in regular Julia code. For example, the assume
function can be used to emit llvm.assume
intrinsic calls:
julia> max(a, b) = a < b ? b : a;
julia> @code_llvm debuginfo=:none max(1,2)
; Function Signature: max(Int64, Int64)
define i64 @julia_max_22435(i64 signext %"a::Int64", i64 signext %"b::Int64") #0 {
top:
%"a::Int64.b::Int64" = call i64 @llvm.smax.i64(i64 %"a::Int64", i64 %"b::Int64")
ret i64 %"a::Int64.b::Int64"
}
julia> function max(a, b)
assume(a > b)
a < b ? b : a
end;
julia> @code_llvm debuginfo=:none max(1,2)
; Function Signature: max(Int64, Int64)
define i64 @julia_max_22441(i64 signext %"a::Int64", i64 signext %"b::Int64") #0 {
top:
%0 = icmp slt i64 %"b::Int64", %"a::Int64"
call void @llvm.assume(i1 %0)
ret i64 %"a::Int64"
}
To abort execution, the trap()
function can be used, generating a call to @llvm.trap
.
Optimization passes
Julia's LLVM passes are usable in the same way as LLVM's passes, and are automatically available to any PassBuilder
. Similarly, the default Julia optimization pipeline can be used through the JuliaPipeline
pipeline object.
julia> run!(JuliaPipeline(), mod)
This object supports many keyword arguments to configure the pipeline; refer to the API documentation for more information.