Metadata

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.

Converting between Metadata and Value

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

Inspecting and attaching

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:

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"}}

Debug information

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: tag
  • DILocation: line, column, scope, inlined_at
  • DIVariable: file, scope, line
  • DIScope: file, name
  • DIFile: directory, filename, source
  • DIType: name, size_in_bits, offset_in_bits, align_in_bits, line, flags
  • DISubprogram: line (and properties inherited from DIScope). Subprograms and the lexical blocks in them are DILocalScopes, the scopes of locations and local variables
  • DIGlobalVariableExpression: 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"].

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>)