Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

LLVM IR Data Types

Recommended resources

In our previous lesson, we began dissecting LLVM IR, familiarizing ourselves with its core structure of functions, basic blocks, and instructions. You learned to trace simple program logic by following instruction results and terminator branches. This laid the groundwork for reading and interpreting the language of the compiler.

This lesson builds directly on that foundation by focusing on a critical aspect you've already encountered: the type system. LLVM IR is a strongly-typed language, meaning every value has a type, and these types are checked rigorously. Your objective is to identify the full range of primitive and derived data types in LLVM IR. A deep understanding of the type system is non-negotiable for analyzing complex IR, which is a daily task when triaging bugs or developing new features in LLVM.

We will systematically explore the building blocks of the type system, from simple integers to complex aggregates, preparing you to understand how any C++ data structure is represented in the intermediate representation.

The Importance of Strong Typing

Before diving into the types themselves, it's worth reinforcing why this matters. In a low-level representation, explicit and strict typing eliminates ambiguity, which is a prerequisite for safe and aggressive optimization. The compiler must know the exact size and properties of the data it's manipulating.

The following short video segment emphasizes this point, showing how types are pervasive in the IR and how the compiler flags mismatches.

2019 EuroLLVM Developers’ Meeting: V. Bridgers & F. Piovezan “LLVM IR Tutorial - Phis, GEPs ...”

This clip from the EuroLLVM 2019 IR Tutorial visually demonstrates the strongly-typed nature of LLVM IR.

Watch the segment from Strongly Typed. Notice how types are specified on nearly every line and how a type mismatch results in a verification error. This is the enforcement that makes LLVM IR so robust.


Primitive Types: The Atoms of Data

Primitive types are the fundamental, indivisible units of data in LLVM IR. All more complex types are built from these.

Integer Types (iN)

You've already seen i32 and the boolean-like i1. The general form is iN, where N is the number of bits. This allows for arbitrary-bit-width integers (e.g., i1, i8, i24, i32, i64).

A crucial point for anyone with a systems programming background is that LLVM IR integers do not have inherent signedness. An i32 is just a collection of 32 bits. Whether it's treated as signed or unsigned depends entirely on the instruction using it. For example:

  • sdiv performs a signed division.
  • udiv performs an unsigned division.
  • add doesn't care about signedness, as the two's complement arithmetic is the same for both.

When you need to change the size of an integer, you must use an explicit conversion instruction:

  • trunc: Truncates a larger integer to a smaller one (e.g., i64 to i32).
  • zext: Zero-extends an integer to a larger size (for unsigned semantics).
  • sext: Sign-extends an integer to a larger size (for signed semantics).

Floating-Point Types

LLVM supports standard floating-point types that correspond to the IEEE 754 standard:

  • float: 32-bit single-precision.
  • double: 64-bit double-precision.
  • Other more exotic types exist, like half, bfloat, fp128, which are important in performance-critical domains like machine learning and high-performance computing.

The void and ptr Types

  • void: Represents the absence of a value. Its only use is as a return type for functions that don't return anything.
  • ptr: Represents a pointer to a memory location. In modern LLVM IR, this is an opaque pointer type. Unlike in C++, where you might have int* or char*, the ptr type itself doesn't specify what it points to. The type of the data is specified by the instruction that uses the pointer, such as load or store. For example, load i32, ptr %my_ptr tells the compiler to load a 32-bit integer from the address in %my_ptr. This design simplifies the IR.

To convert between pointers and integers, you use ptrtoint and inttoptr. To cast between pointer types (which is less common with opaque pointers but still necessary sometimes, especially when interacting with legacy code or specific intrinsics), you use bitcast.

The following reading selection provides a comprehensive tour of these types and their associated conversion instructions.

A Gentle Introduction to LLVM IR - mcyoung

This article provides an excellent narrative on the LLVM type system. We'll read a few sections to cover all the primitive types and their conversions.

First, read the section "Purely Scalar Code" for a discussion on integer types and the trunc, zext, and sext conversion instructions. Next, read the first three paragraphs of the "Types and Aggregates" section. This covers integer (iN), floating-point, void, and ptr types. Finally, read the section on "Reinterpret Shenanigans" to understand bitcast, inttoptr, and ptrtoint.


Derived Types: Composing Data

Derived types, also known as composite types, are aggregates built from primitive types or other derived types. They allow you to represent complex data structures from your source language.

Array Types

An array is a sequence of elements of the same type. The syntax is [<number_of_elements> x <element_type>]. For example, [10 x i32] is an array of ten 32-bit integers. The size is fixed and known at compile time.

Struct Types

A struct represents a collection of elements that can have different types. The syntax is { <type1>, <type2>, ... }. For example, { i8, i32, ptr } could represent a simple struct. Unlike C structs, the fields in LLVM IR are anonymous and are accessed by a zero-based index.

For fine-grained memory layout control, which is often critical in systems and performance programming, LLVM also provides packed structs. These are written as <{ ... }> and instruct the compiler to lay out the fields without any padding, even if it means unaligned access.

Vector Types

A vector is a sequence of elements of the same primitive type, similar to an array. However, its purpose is different. The syntax is < <number_of_elements> x <element_type> >, for example, <4 x float>.

Vectors are specifically for SIMD (Single Instruction, Multiple Data) operations. A single instruction can operate on all elements of a vector in parallel. For example, an fadd on two <4 x float> vectors would perform four additions simultaneously on a CPU with appropriate hardware support. This is a cornerstone of high-performance code generation.

In the Fibonacci example from our previous lesson, you can now clearly identify the `i64` integer type used for the argument and calculations, and the `i1` type for the result of the comparison instruction.

We will now turn back to the "A Gentle Introduction to LLVM IR" article to see how these derived types are defined.

A Gentle Introduction to LLVM IR - mcyoung

This section details the syntax for arrays, structs, packed structs, and vectors.

Read the part of the "Types and Aggregates" section starting from the discussion of arrays. Pay close attention to the syntax for each derived type.


Identifying Types in Practice

Let's look at the LLVM IR Quick Reference for a concise, tabular summary of the types we've discussed. This format is useful for quick lookups when you're analyzing IR files.

[PDF] LLVM IR Quick Reference.

This PDF provides a formal breakdown of the LLVM IR type system.

Skim through Chapter 3.1, "Primitive Types" (pages 55-60), to see a structured definition of the primitives. Then, skim Chapter 3.2, "Composite Types" (pages 61-67), for the formal definitions of arrays, structs, and vectors. Finally, look at the "Data Types" table in Appendix L (page 160) for a quick summary.


Conclusion

In this lesson, you've taken a deep dive into the LLVM IR type system, a crucial step toward fluency in the language of the compiler. You now have the vocabulary to describe the data being manipulated in any piece of IR you encounter.

Key Takeaways:

  • Strongly Typed: LLVM IR is strictly typed, which enables safe and powerful optimizations.
  • Primitive Types: The basic building blocks are iN (arbitrary-width integers), float/double (floating-point), void (absence of a value), and ptr (opaque pointer).
  • Signless Integers: Integer types themselves have no sign; signedness is determined by the instructions that operate on them.
  • Derived Types: Complex data structures are represented using [N x T] (arrays), {...} (structs), and <N x T> (vectors for SIMD).
  • Access by Index: Elements of arrays and fields of structs are accessed by a numerical index.

In our next lesson, we will tackle control flow in more detail. You'll learn how LLVM represents conditional logic and loops using br instructions and, most importantly, how phi instructions are used to merge values from different control flow paths, a concept that brings together SSA, branching, and the type system.