Assemble LLVM IR to Bitcode with llvm-as-20

llvm-as-20 turns readable LLVM IR into bitcode, and the terminal will thank you for redirecting the result to a file. You will finish with a repeatable way to do that, choose the output explicitly, and check the result without accidentally dumping binary data onto your screen. This guide uses the installed LLVM 20.1.8 build from package llvm-20.

Allow about fifteen minutes. You need a shell, the llvm-20 package, and an input file containing valid LLVM IR. These are ordinary user commands: they write files where you tell them to, and no elevated privileges are needed unless your chosen output directory is not writable by your account.

1. Check the installed assembler

Confirm which executable is available and record its version before relying on option details:

$ command -v llvm-as-20
/usr/bin/llvm-as-20
$ llvm-as-20 --version
Ubuntu LLVM version 20.1.8
  Optimized build.

The package version can carry a distribution build suffix, so quote the command version when documenting the behaviour you actually tested. The installed manpage describes llvm-as as the LLVM assembler: it reads textual assembly, produces bitcode, and writes that result to a file or standard output.

Checkpoint: if command -v finds a different executable, stop and check that its version matches the command used through the rest of this guide.

2. Create a small, valid IR input

Use a file ending in .ll for normal source input. This minimal module defines a function that returns zero:

; ModuleID = "demo"
define i32 @main() {
entry:
  ret i32 0
}

Save that text as demo.ll in a working directory. Keep the source file: bitcode is compact and meant for tools, while the .ll file is the useful form for inspection and review.

LLVM IR is syntax-sensitive. A misspelled instruction, a missing type or a malformed block label causes a non-zero exit status and an error pointing at the input, so do not treat a file that merely looks LLVM-shaped as valid.

3. Assemble with an explicit output path

Choose the destination with -o. It makes scripts and reviews easier because the output location is visible right there in the command:

$ llvm-as-20 -o demo.bc demo.ll
$ file demo.bc
demo.bc: LLVM IR bitcode

A successful assembly exits with status zero and normally prints nothing. The input stays unchanged; the command creates or replaces the output path. Replacing an existing bitcode file is a state-changing action, so check the destination first when it might hold work from another build:

$ test -e demo.bc && printf '%s\n' 'demo.bc already exists'
$ llvm-as-20 -o demo.bc demo.ll

If the destination matters, use a new filename or move the old file aside before running the command. There is no undo built into llvm-as-20: recovery means rebuilding from the retained .ll source, or restoring the previous output from your normal backup or version-control workflow.

4. Verify the bitcode by disassembling it

Use the matching LLVM disassembler to confirm the output reads back cleanly:

$ llvm-dis-20 demo.bc -o -
; ModuleID = 'demo.bc'
source_filename = "demo.ll"

define i32 @main() {
entry:
  ret i32 0
}

The exact module identifier and source filename can vary with the path and LLVM build. The useful check is that llvm-dis-20 exits successfully and shows the function you assembled. If it fails, look at the assembler's exit status and confirm the consumer comes from a compatible LLVM installation.

Warning: do not use a terminal as the destination for raw bitcode. Without -f, the assembler refuses binary output when the output stream is a terminal, because binary bytes make a terminal unreadable and can trigger undesirable terminal behaviour.

5. Use the default output naming when it helps

When -o is omitted, the manpage defines three cases:

This uses the second rule and creates demo.bc beside demo.ll:

$ llvm-as-20 demo.ll
$ test -s demo.bc
$ printf '%s\n' 'bitcode output exists'
bitcode output exists

For a pipeline, make the stream behaviour explicit. This sends binary output to standard output, so redirect it to a file rather than leaving it attached to the terminal:

$ llvm-as-20 -o - < demo.ll > demo-from-stdin.bc
$ llvm-dis-20 demo-from-stdin.bc -o - | grep -F 'ret i32 0'
  ret i32 0

With no input filename, or a filename of -, the assembler reads standard input. With -o -, it writes standard output. The two choices are independent, which is useful in build pipelines but makes an accidental terminal write easy if you forget the output redirection.

6. Separate checking from writing

LLVM 20.1.8 also accepts --disable-output. Use it when a build or validation step should parse and assemble the module but must not create bitcode:

$ llvm-as-20 --disable-output demo.ll
$ printf 'assembler status: %s\n' "$?"
assembler status: 0

Handy for syntax checks. It does not repair invalid IR, and it does not prove a later consumer will accept every target-specific property, it only stops this particular invocation from writing its normal output.

--module-hash asks LLVM to emit a module hash as part of the generated bitcode. Use it when a downstream LLVM workflow expects that metadata, not as a replacement for a content digest or a signature. --data-layout=<layout-string> supplies a data layout for specialised input. Both are version-sensitive: check llvm-as-20 --help on the machine running the build and document the choice beside the build rule.

7. Diagnose the common failures

Capture the exit status immediately after a failed command:

$ llvm-as-20 -o demo.bc broken.ll
llvm-as-20: broken.ll:1:1: error: expected top-level entity
$ status=$?
$ printf 'assembler status: %s\n' "$status"
assembler status: 1

The line, column and wording depend on the error. Fix the textual IR and rerun the command. A non-zero status means the output must not be treated as valid, even if an older demo.bc already sits at that path. Check its timestamp, or build to a fresh filename when stale output would be dangerous.

If an output path cannot be opened, check its parent directory, permissions and whether the destination is actually a directory. Use sudo only when the intended directory genuinely requires elevated access, it is usually safer to assemble in a user-writable build directory and install the finished artefact separately through the system's normal package or deployment process.

Done means