DocTestSetup = quote
using LLVM, LLVM.IR, LLVM.Build, LLVM.Passes, LLVM.ORC
if context(; throw_error=false) === nothing
Context()
end
end
Metadata is a way to attach additional information to various entities in LLVM IR. Various
metadata object types exist, each prefixed with MD, and with a variety of APIs to query
and manipulate them. The abstract supertype of all metadata objects is Metadata.
Metadata strings can be constructed with the MDString constructor, and converted back to
Julia strings with a convert call:
julia> md = MDString("hello")
!"hello"
julia> convert(String, md)
"hello"
Then there are metadata nodes, which can have other metadata as operands. For example, a
tuple of metadata nodes can be created with the following MDNode constructor:
julia> md = MDNode([MDString("hello"), MDString("world")])
<0x6000022c7f20> = !{!"hello", !"world"}
These operands are available as the operands property, which is a view of the node:
julia> md.operands[1]
!"hello"
julia> collect(md.operands)
2-element Vector{Union{Nothing, Metadata}}:
!"hello"
!"world"
The view is mutable, so assigning to an element replaces that operand of the node. LLVM
keeps uniqued nodes like this one unique, so if the change makes the node identical to an
existing one, the node is made distinct instead.
It happens often that you want to put values in metadata, or use metadata with APIs that
expect a value. In LLVM.jl, this is possible by using the Value and Metadata
constructors with respectively Metadata and Value inputs, automatically wrapping them in
the correct type:
julia> val = ConstantInt(42);
julia> md = Metadata(val)
i64 42
julia> typeof(md)
LLVM.ConstantAsMetadata
julia> md = MDString("test");
julia> val = Value(md)
!"test"
julia> typeof(val)
MetadataAsValue
Any global object (a function or global variable) and instruction can have metadata attached
to it. In LLVM.jl, it's possible to inspect and mutate that metadata using the metadata
property, a dictionary-like view that maps the kind of metadata to a metadata node:
DocTestSetup = quote
using LLVM, LLVM.IR, LLVM.Build, LLVM.Passes, LLVM.ORC
if context(; throw_error=false) === nothing
Context()
end
ir = """
define i64 @"julia_+"(i64 signext %0, i64 signext %1) {
top:
%2 = add i64 %1, %0
ret i64 %2
}"""
mod = parse(LLVM.Module, ir)
add = only(mod.functions)
bb = add.entry
inst = first(bb.instructions)
end
julia> inst
%2 = add i64 %1, %0
julia> isempty(inst.metadata)
true
julia> inst.metadata["dbg"] = MDNode([MDString("hello")])
<0x5ff68c3f2f28> = !{!"hello"}
julia> inst
%2 = add i64 %1, %0, !dbg !0
Metadata can also be attached to a module, in which case it needs to be grouped in a named
metadata node (which can only contain other metadata nodes, and not e.g. strings directly).
The named metadata of a module is available as its metadata property, a dictionary-like
view. Looking up a name that doesn't exist throws a KeyError, while get! creates an
empty named metadata node. The operands of a named metadata node are a mutable view, so
operands can be appended with push!, replaced by assigning to them, and removed with
empty!:
julia> md = mod.metadata;
julia> isempty(md)
true
julia> push!(get!(md, "hello").operands, MDNode([MDString("world")]));
julia> md
ModuleMetadataIterator for module :
!hello = !{<0x60000394d3f8> = !{!"world"}}
Assigning to obj.metadata[kind] replaces the attachments of that kind. Global objects can
have several attachments of one kind, e.g., a global variable that is described by several
!dbg expressions, and add_metadata! adds one while keeping the others:
julia> gv = GlobalVariable(mod, LLVM.Int32Type(), "gv");
julia> add_metadata!(gv, "annotation", MDNode([MDString("first")]));
julia> add_metadata!(gv, "annotation", MDNode([MDString("second")]))
@gv = external global i32, !annotation !0, !annotation !1
Some metadata of an instruction only holds where the instruction is: e.g., a load can be
annotated with the !range of values it loads because a branch checked it. A pass that
moves such an instruction elsewhere needs to drop that metadata, which
drop_unknown_metadata! does for all kinds except for debug information and the kinds
that are passed to it:
julia> mod = parse(LLVM.Module, """
define i32 @f(ptr %p) {
%v = load i32, ptr %p, !range !0, !noundef !1
ret i32 %v
}
!0 = !{i32 0, i32 10}
!1 = !{}""");
julia> load = first(only(mod.functions).entry.instructions)
%v = load i32, ptr %p, align 4, !range !0, !noundef !1
julia> drop_unknown_metadata!(load, ("noundef",));
julia> load
%v = load i32, ptr %p, align 4, !noundef !0
When IR is cloned or remapped (see IR cloning), the metadata it refers to is
mapped as well, using map_metadata.
When generating IR, it is possible to add debug information metadata to the generated code. This information can be used by debuggers to provide a better debugging experience.
Debug information is created with a DIBuilder, whose functions (file!,
compile_unit!, subprogram!, basic_type!, auto_variable!, dbg_value!, ...) are
part of the LLVM.Build vocabulary, and listed in the reference. Disposing of the builder
finalizes the debug info; call finalize! to do so earlier, e.g., before emitting code.
Types that refer to themselves are built using temporary nodes (e.g.,
replaceable_composite_type!), which are TemporaryMDNodes that need to be replaced with
replace_temporary! before the builder is finalized.
dbg_declare! and dbg_value! describe the location of a source variable at an insertion
point. Since LLVM 19, that information is stored in debug records attached to instructions
(inst.debug_records, which contains DbgRecords); older versions of LLVM use calls to
the llvm.dbg.* intrinsics instead.
LLVM represents debug information as a variety of DI-prefixed structures, which are
subtypes of the above metadata types. In LLVM.jl, these structures expose their contents
as properties:
DINode:tagDILocation:line,column,scope,inlined_atDIVariable:file,scope,lineDIScope:file,nameDIFile:directory,filename,sourceDIType:name,size_in_bits,offset_in_bits,align_in_bits,line,flagsDISubprogram:line(and properties inherited fromDIScope). Subprograms and the lexical blocks in them areDILocalScopes, the scopes of locations and local variablesDIGlobalVariableExpression:variable,expression
The debug location of an instruction is its inst.debug_location property, which is also
available as its !dbg metadata, inst.metadata["dbg"].
DocTestSetup = quote
using LLVM, LLVM.IR, LLVM.Build, LLVM.Passes, LLVM.ORC
if context(; throw_error=false) === nothing
Context()
end
ir = """
define i64 @"julia_+"(i64 signext %0, i64 signext %1) !dbg !5 {
top:
%2 = add i64 %1, %0, !dbg !15
ret i64 %2, !dbg !15
}
!llvm.module.flags = !{!0, !1, !2}
!llvm.dbg.cu = !{!3}
!0 = !{i32 2, !"Dwarf Version", i32 4}
!1 = !{i32 2, !"Debug Info Version", i32 3}
!2 = !{i32 2, !"julia.debug_level", i32 1}
!3 = distinct !DICompileUnit(language: DW_LANG_Julia, file: !4, producer: "julia", isOptimized: true, runtimeVersion: 0, emissionKind: NoDebug, nameTableKind: GNU)
!4 = !DIFile(filename: "julia", directory: ".")
!5 = distinct !DISubprogram(name: "+", linkageName: "julia_+", scope: null, file: !6, line: 87, type: !7, scopeLine: 87, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !3, retainedNodes: !8)
!6 = !DIFile(filename: "int.jl", directory: ".")
!7 = !DISubroutineType(types: !8)
!8 = !{}
!15 = !DILocation(line: 87, scope: !5)"""
mod = parse(LLVM.Module, ir)
add = only(mod.functions)
bb = add.entry
inst = first(bb.instructions)
end
julia> inst
%2 = add i64 %1, %0, !dbg !9
julia> dbg = inst.metadata["dbg"]
!DILocation(line: 87, scope: <0x6000056c5dd0>) = !DILocation(line: 87, scope: <0x6000056c5dd0>)
julia> dbg.line
87
julia> dbg.scope.file
<0x6000000beb80> = !DIFile(filename: "int.jl", directory: ".")
Debug info can also be attached to functions, which can be queried and modified using the
subprogram property:
julia> sp = add.subprogram
<0x600003edfad0> = distinct !DISubprogram(name: "+", linkageName: "julia_+", scope: null, file: <0x600003ba6fe0>, line: 87, type: <0x600003494c90>, scopeLine: 87, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: <0x6000021d8428>, retainedNodes: <0x6000010f09d0>)