LLVM & MLIR Workflows
Writing a front-end that generates LLVM IR code and using LLVM to optimize and compile it:
To write a front-end that generates LLVM IR code, you will need to use a programming language that can generate LLVM IR code. For example, you can use C, C++, or Rust.
Here is an example of a C program that generates LLVM IR code:
#include <llvm-c/Core.h>
int main() {
LLVMModuleRef module = LLVMModuleCreateWithName("my_module");
LLVMTypeRef int_type = LLVMInt32Type();
LLVMValueRef hello_world = LLVMBuildGlobalStringPtr(module, "Hello, world!", "hello_world");
LLVMTypeRef main_type = LLVMFunctionType(int_type, NULL, 0, 0);
LLVMValueRef main_func = LLVMAddFunction(module, "main", main_type);
LLVMBasicBlockRef entry = LLVMAppendBasicBlock(main_func, "entry");
LLVMBuilderRef builder = LLVMCreateBuilder();
LLVMBuildCall(builder, LLVMGetNamedFunction(module, "puts"), &hello_world, 1, "");
LLVMBuildRet(builder, LLVMConstInt(int_type, 0, 0));
LLVMDisposeBuilder(builder);
LLVMPrintModuleToFile(module, "my_module.ll", NULL);
LLVMDisposeModule(module);
return 0;
}
This program creates a new LLVM module, adds a global string constant, creates a main function that calls the puts function to print the string, and then returns 0. Finally, it prints the LLVM IR code to a file called my_module.ll. Optimizing and Compiling with LLVM
Once you have generated LLVM IR code, you can use LLVM to optimize and compile it to machine code. Here is an example of how to do this:
Optimize the LLVM IR code
opt -O3 my_module.ll -o my_module.opt.ll
Compile the optimized LLVM IR code to machine code
llc my_module.opt.ll -o my_module.o
Link the object file to create an executable
clang my_module.o -o my_module
This example uses the opt command to optimize the LLVM IR code with -O3 optimization level, then uses the llc command to compile the optimized LLVM IR code to a machine code object file. Finally, it uses the clang command to link the object file and create an executable. Using MLIR as an Intermediate Representation
This is a great example of a C program that generates LLVM IR code. Here's a brief overview of what the program does: Creates an LLVM module named "my_module" using the LLVMModuleCreateWithName function. Defines an integer type using the LLVMInt32Type function and creates a global string pointer containing the message "Hello, world!" using the LLVMBuildGlobalStringPtr function. Defines a function type using the LLVMFunctionType function and creates a main function using the LLVMAddFunction function. It also creates a basic block for the main function using the LLVMAppendBasicBlock function and creates a builder using the LLVMCreateBuilder function. Uses the LLVMBuildCall function to call the "puts" function with the "hello_world" global string pointer as an argument, and uses the LLVMBuildRet function to return 0. Disposes of the builder using the LLVMDisposeBuilder function and prints the module to a file named "my_module.ll" using the LLVMPrintModuleToFile function. Disposes of the module using the LLVMDisposeModule function and returns 0.
Overall, this program demonstrates how C code can be used to generate LLVM IR code, which can then be used for a variety of purposes, such as code optimization and just-in-time compilation.
Makefile that automates the build steps for the LLVM code:
# Compiler and linker settings
CC = clang
LLC = llc
OPT = opt
# LLVM module and IR file names
LLVM_MODULE = my_module.ll
OPT_LLVM_MODULE = my_module.opt.ll
# Object and executable file names
OBJ = my_module.o
EXE = my_module
# Flags for the compiler and linker
CFLAGS = -g -Wall
LDFLAGS =
all: $(EXE)
$(EXE): $(OBJ)
$(CC) $(LDFLAGS) $< -o $@
$(OBJ): $(LLVM_MODULE)
$(LLC) $< -filetype=obj -o $@
$(OPT_LLVM_MODULE): $(LLVM_MODULE)
$(OPT) -O3 $< -o $@
$(LLVM_MODULE): main.c
clang -S -emit-llvm $< -o $@
clean:
rm -f $(EXE) $(OBJ) $(LLVM_MODULE) $(OPT_LLVM_MODULE)
This Makefile defines the compiler and linker settings, as well as the names of the LLVM module, optimized LLVM module, object, and executable files. It also defines the flags for the compiler and linker.
The "all" target is the default target and depends on the $(EXE) target. The $(EXE) target depends on the $(OBJ) target, and the $(OBJ) target depends on the $(LLVM_MODULE) target.
The $(LLVM_MODULE) target compiles the C code to LLVM IR code using the clang compiler with the "-S -emit-llvm" flags. The $(OPT_LLVM_MODULE) target optimizes the LLVM IR code using the opt tool with the "-O3" flag. The $(OBJ) target converts the optimized LLVM IR code to an object file using llc. The $(EXE) target links the object file and generates an executable using the compiler.
The "clean" target removes all generated files.
To use this Makefile, simply save it as a file named "Makefile" in the same directory as your C code, and run the command "make" on the command line. This will automatically compile the C code to LLVM IR code, optimize it, convert it to an object file, link it, and generate an executable.
Here's the equivalent MLIR code for the given LLVM code:
module {
func @main() -> i32 {
%0 = llvm.global_string "Hello, world!"
%1 = llvm.call @puts(%0)
llvm.return 0 : i32
}
}
This MLIR code has a single function named "main" that returns a 32-bit integer. The function contains two instructions: the first instruction creates a global string containing the message "Hello, world!" and stores it in %0. The second instruction calls the "puts" function with %0 as an argument and stores the result in %1. Finally, the function returns 0 using the "llvm.return" instruction.
Note that this MLIR code uses the LLVM dialect, which is a dialect of MLIR that represents LLVM IR instructions. This dialect is designed to be compatible with LLVM IR, which means that you can use MLIR to generate LLVM IR code, just like in the original C code.
Here's the completed code snippet:
#include <mlir-c/IR.h>
int main() {
// Create a new MLIR context.
MLIRContextRef context = mlirContextCreate();
// Create a new MLIR module.
MLIRModuleRef module = mlirModuleCreateEmpty("my_module", context);
// Create a location.
MLIRLocationRef loc = mlirLocationUnknownGet(context);
// Create an MLIR integer type.
MLIRTypeRef int_type = mlirIntegerTypeGet(context, 32);
// Create an MLIR string attribute.
MLIRAttributeRef hello_world_str = mlirStringAttrGet(context, "Hello, world!");
// Create an MLIR string type.
MLIRTypeRef str_type = mlirStringTypeGet(context);
// Create an MLIR global string operation.
MLIROperationState hello_world_op_state = mlirOperationStateGet("", loc);
mlirOperationStateAddResults(&hello_world_op_state, &str_type, 1);
mlirOperationStateAddAttributes(&hello_world_op_state, &hello_world_str, 1);
mlirOperationStateSetOpcode(&hello_world_op_state, "std.constant");
MLIRValueRef hello_world_val = mlirOperationCreate(&hello_world_op_state);
mlirModuleAppendOperation(module, hello_world_val);
// Create an MLIR function type.
MLIRTypeRef main_type = mlirFunctionTypeGet(context, 0, NULL, 0);
// Create an MLIR function operation.
MLIRAttributeRef func_name = mlirStringAttrGet(context, "main");
MLIROperationState main_func_state = mlirOperationStateGet("", loc);
mlirOperationStateAddResults(&main_func_state, &int_type, 1);
mlirOperationStateAddAttributes(&main_func_state, &func_name, 1);
mlirOperationStateSetAttrByName(&main_func_state, "type", mlirTypeAttrGet(main_type));
mlirOperationStateSetOpcode(&main_func_state, "func");
MLIRFuncOp main_func = mlirOperationCreate(&main_func_state);
mlirModuleAppendOperation(module, main_func);
// Create an MLIR return operation.
MLIROperationState ret_op_state = mlirOperationStateGet("", loc);
mlirOperationStateAddOperands(&ret_op_state, &hello_world_val, 1);
mlirOperationStateSetOpcode(&ret_op_state, "std.return");
MLIROperationRef ret_op = mlirOperationCreate(&ret_op_state);
mlirBlockAppendOwnedOperation(mlirBlockGetTerminator(mlirFuncOpGetBody(main_func)), ret_op);
// Print the MLIR module.
mlirModulePrint(module, stdout);
// Clean up.
mlirOperationDestroy(hello_world_val);
mlirOperationDestroy(main_func);
mlirContextDestroy(context);
return 0;
}
This code creates an MLIR module that contains a single function named "main". The function returns a constant string value containing the message "Hello, world!" using the "std.constant" operation, and then returns that value using the "std.return" operation. Finally, the module is printed to stdout using the "mlirModulePrint" function.
Note that this code uses the "std" dialect, which is a standard dialect of MLIR that provides a set of common operations and types. You can define your own dialects and operations in MLIR to create custom languages and optimizations.
To compile an MLIR module to an executable using LLVM, you need to use the following command line tools:
- mlir-translate: This tool is used to translate MLIR code to LLVM IR code.
- llc: This tool is used to convert LLVM IR code to object files.
- clang or gcc: These tools are used to link the object files and generate an executable.
Here are the commands to compile the MLIR module generated by the code snippet I provided earlier: 4. Translate the MLIR module to LLVM IR code using mlir-translate:
- mlir-translate my_module.mlir -mlir-to-llvmir > my_module.ll
- Compile the LLVM IR code to an object file using llc:
- llc my_module.ll -filetype=obj -o my_module.o
- Link the object file and generate an executable using clang or gcc:
- clang my_module.o -o my_module
This will generate an executable file named "my_module" that you can run on the command line. Note that you may need to add additional flags to the clang or gcc command to link any required libraries or include paths.
Makefile that automates the LLVM to module build steps:
# Compiler and linker settings
CC = clang
LLC = llc
MLIR_TRANSLATE = mlir-translate
# MLIR module and LLVM IR file names
MLIR_MODULE = my_module.mlir
LLVM_IR = my_module.ll
# Object and executable file names
OBJ = my_module.o
EXE = my_module
# Flags for the compiler and linker
CFLAGS = -g -Wall
LDFLAGS =
all: $(EXE)
$(EXE): $(OBJ)
$(CC) $(LDFLAGS) $< -o $@
$(OBJ): $(LLVM_IR)
$(LLC) $< -filetype=obj -o $@
$(LLVM_IR): $(MLIR_MODULE)
$(MLIR_TRANSLATE) $< -mlir-to-llvmir > $@
clean:
rm -f $(EXE) $(OBJ) $(LLVM_IR)
This Makefile defines the compiler and linker settings, as well as the names of the MLIR module, LLVM IR, object, and executable files. It also defines the flags for the compiler and linker.
The "all" target is the default target and depends on the $(EXE) target. The $(EXE) target depends on the $(OBJ) target, and the $(OBJ) target depends on the $(LLVM_IR) target.
The $(LLVM_IR) target uses mlir-translate to translate the MLIR module to LLVM IR code. The $(OBJ) target uses llc to convert the LLVM IR code to an object file. The $(EXE) target uses the compiler to link the object file and generate an executable.
The "clean" target removes all generated files.
To use this Makefile, simply save it as a file named "Makefile" in the same directory as your MLIR module, and run the command "make" on the command line. This will automatically build the MLIR module and generate an executable.