Step 3 — Install Homebrew LLVM (The Full Toolchain)

Goal

Install upstream LLVM via Homebrew. This gives us the headers, libraries, and command-line tools (opt, llc, lli, mlir-opt, llvm-config) that Apple Clang does not include. Required from Phase 5 onward.

Why Apple Clang Isn't Enough

You already have Apple Clang from Step 1. It compiles C++ fine. But it lacks:

  • libLLVM.dylib C++ headers — needed to write a compiler that emits LLVM IR programmatically (Phase 5).
  • llvm-config — the helper that tells CMake's find_package(LLVM) where to look.
  • opt — the LLVM optimizer driver. opt -O3 input.ll > output.ll.
  • llc — the LLVM static compiler. Lowers .ll → target assembly.
  • lli — the LLVM IR interpreter/JIT. Run .ll files directly.
  • mlir-opt, mlir-translate — MLIR's opt and bridge to LLVM (Phase 7).
  • lld — LLVM's linker. Faster than system ld, used for cross-linking.

Apple intentionally omits these because Apple Clang is a product, not a development SDK. Homebrew LLVM fills the gap.

Install

brew install llvm

This installs to /opt/homebrew/opt/llvm/ on Apple Silicon (or /usr/local/opt/llvm/ on Intel Macs). Homebrew deliberately does NOT symlink to /usr/local/bin to avoid shadowing Apple Clang.

Disk: ~3 GB. Time: 5–10 minutes (cached binary download).

Make It Discoverable

Homebrew prints instructions; the relevant parts:

# Add to your ~/.zshrc (or ~/.bashrc):
export LLVM_HOME="/opt/homebrew/opt/llvm"
export PATH="$LLVM_HOME/bin:$PATH"

# For CMake's find_package(LLVM) and find_package(MLIR):
export LLVM_DIR="$LLVM_HOME/lib/cmake/llvm"
export MLIR_DIR="$LLVM_HOME/lib/cmake/mlir"

# For dyld to find LLVM libraries at runtime (rarely needed; CMake usually handles rpath):
# export DYLD_LIBRARY_PATH="$LLVM_HOME/lib:$DYLD_LIBRARY_PATH"

After editing, reload:

source ~/.zshrc

Why prepend $LLVM_HOME/bin to PATH? So clang++ and clang refer to Homebrew LLVM during compiler-engineering work. Apple Clang is still at /usr/bin/clang++ for any tool that hard-codes that path.

Verify

which clang++
# Expected: /opt/homebrew/opt/llvm/bin/clang++

clang++ --version
# Expected: clang version 18.x.x or 19.x.x or 20.x.x
#           (NOT "Apple clang")

llvm-config --version
# Expected: 18.x.x / 19.x.x / 20.x.x (matches clang++)

llvm-config --prefix
# Expected: /opt/homebrew/opt/llvm

llc --version | head -3
opt --version | head -3
lli --version | head -3
mlir-opt --version | head -3

All four should print a version line. If mlir-opt: command not found, your Homebrew LLVM is too old or wasn't built with MLIR. brew upgrade llvm should fix it (current Homebrew LLVM bottles include MLIR by default).

Inspect What's Available

ls "$LLVM_HOME/bin" | head -30
# clang, clang++, lld, ld64.lld, llc, lli, llvm-ar, llvm-as, llvm-cov, llvm-dis,
# llvm-dwarfdump, llvm-link, llvm-mc, llvm-nm, llvm-objcopy, llvm-objdump,
# llvm-readelf, llvm-readobj, llvm-rtdyld, llvm-symbolizer, llvm-undname,
# mlir-cpu-runner, mlir-opt, mlir-tblgen, mlir-translate, opt, ...

Every one of these is a tool you'll meet at some point.

The llvm-config Story

llvm-config is a small helper that prints LLVM build information. Try:

llvm-config --includedir
# /opt/homebrew/opt/llvm/include

llvm-config --libdir
# /opt/homebrew/opt/llvm/lib

llvm-config --libs core support irreader
# -lLLVMIRReader -lLLVMBitReader -lLLVMAsmParser -lLLVMCore -lLLVMRemarks ...

llvm-config --cxxflags
# -I/opt/homebrew/opt/llvm/include -std=c++17 -fno-exceptions -fno-rtti ...

CMake's find_package(LLVM) internally calls llvm-config to discover all of this. As long as LLVM_DIR is set (or llvm-config is on PATH), CMake "just works".

Try It — Compile and Run LLVM IR

Save this as /tmp/hello.ll:

@.str = private unnamed_addr constant [14 x i8] c"hello, llvm!\0A\00"

declare i32 @printf(ptr, ...)

define i32 @main() {
  %1 = call i32 (ptr, ...) @printf(ptr @.str)
  ret i32 0
}

Run it three different ways — each demonstrates a different tool:

# 1. Interpret/JIT directly (lli)
lli /tmp/hello.ll
# → hello, llvm!

# 2. Compile to assembly (llc), then assemble + link (clang++)
llc /tmp/hello.ll -o /tmp/hello.s
clang++ /tmp/hello.s -o /tmp/hello-static && /tmp/hello-static
# → hello, llvm!

# 3. Optimize, then compile (opt + llc)
opt -O3 /tmp/hello.ll -S -o /tmp/hello.opt.ll
llc /tmp/hello.opt.ll -o /tmp/hello.opt.s
clang++ /tmp/hello.opt.s -o /tmp/hello-opt && /tmp/hello-opt
# → hello, llvm!

You just performed all three roles of LLVM: as an interpreter (lli), as a static compiler (llc), and as an optimizer (opt). Every later lab will revisit these tools.

What Just Happened

  1. You installed upstream LLVM separately from Apple Clang.
  2. You prepended its bin to PATH so clang++ now refers to Homebrew's, NOT Apple's.
  3. You exported LLVM_DIR so CMake's find_package(LLVM) works without extra flags.
  4. You wrote raw LLVM IR by hand, then exercised lli, llc, and opt — proof that the toolchain is wired correctly.

Common Pitfalls

  • clang++ --version still says "Apple clang": you forgot to source ~/.zshrc or you opened a new terminal that doesn't load it. Check with echo $PATH | tr : '\n' | head.
  • mlir-opt: command not found: very old Homebrew LLVM (pre-15). Run brew upgrade llvm.
  • dyld: Library not loaded: @rpath/libLLVM.dylib: a binary you built can't find its libs at runtime. Either add $LLVM_HOME/lib to DYLD_LIBRARY_PATH, or have CMake set INSTALL_RPATH (set(CMAKE_INSTALL_RPATH "${LLVM_HOME}/lib")).
  • CMake can't find_package(LLVM): confirm LLVM_DIR is exported. Try cmake -DLLVM_DIR="$LLVM_HOME/lib/cmake/llvm" ... as a one-off.

Next

04-verify-end-to-end.md